diff --git a/deadsands/pyproject.toml b/deadsands/pyproject.toml index 53cb2ec..6f8ce26 100644 --- a/deadsands/pyproject.toml +++ b/deadsands/pyproject.toml @@ -40,6 +40,29 @@ dmsh = "site_tools.cli:dmsh" +[tool.poetry.dev-dependencies] +black = "^23.3.0" +isort = "^5.12.0" +pyproject-autoflake = "^1.0.2" + [build-system] requires = ['poetry-core~=1.0'] build-backend = 'poetry.core.masonry.api' + +[tool.black] +line-length = 120 +target-version = ['py310'] + +[tool.isort] +multi_line_output = 3 +line_length = 120 +include_trailing_comma = true + +[tool.autoflake] +check = false # return error code if changes are needed +in-place = true # make changes to files instead of printing diffs +recursive = true # drill down directories recursively +remove-all-unused-imports = true # remove all unused imports (not just those from the standard library) +ignore-init-module-imports = true # exclude __init__.py when removing unused imports +remove-duplicate-keys = true # remove all duplicate keys in objects +remove-unused-variables = true # remove unused variables diff --git a/deadsands/site_tools/__init__.py b/deadsands/site_tools/__init__.py index 7fd68b7..000c258 100644 --- a/deadsands/site_tools/__init__.py +++ b/deadsands/site_tools/__init__.py @@ -2,8 +2,8 @@ from pelican.settings import DEFAULT_CONFIG, get_settings_from_file OPEN_BROWSER_ON_SERVE = True -DEV_SETTINGS_FILE_BASE = 'pelicanconf.py' -PUB_SETTINGS_FILE_BASE = 'publishconf.py' +DEV_SETTINGS_FILE_BASE = "pelicanconf.py" +PUB_SETTINGS_FILE_BASE = "publishconf.py" SETTINGS = {} diff --git a/deadsands/site_tools/cli.py b/deadsands/site_tools/cli.py index 84d6112..f9ea09e 100644 --- a/deadsands/site_tools/cli.py +++ b/deadsands/site_tools/cli.py @@ -1,102 +1,95 @@ import asyncio -import click import os +import shlex import shutil import subprocess -import shlex import sys -import typer -import webbrowser import termios - -import site_tools as st - +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 pathlib import Path from pelican import main as pelican_main -from time import sleep +from rolltable.tables import RollTable from typing_extensions import Annotated -from collections import defaultdict +import site_tools as st from site_tools.content_manager import create from site_tools.shell.interactive_shell import DMShell -from rolltable.tables import RollTable - 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', -}) +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' - lore = 'lore' - monster = 'monster' - region = 'region' - location = 'location' - page = 'page' - + post = "post" + lore = "lore" + monster = "monster" + region = "region" + location = "location" + page = "page" class Die(str, Enum): - d100 = '100' - d20 = '20' - d12 = '12' - d10 = '10' - d6 = '6' - d4 = '4' - + d100 = "100" + d20 = "20" + d12 = "12" + d10 = "10" + 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) - + settings = CONFIG["settings_publish" if publish else "settings_base"] + pelican_main(["-s", settings] + cmd) @app.command() def clean() -> None: - if os.path.isdir(CONFIG['deploy_path']): - shutil.rmtree(CONFIG['deploy_path']) - os.makedirs(CONFIG['deploy_path']) - + if os.path.isdir(CONFIG["deploy_path"]): + shutil.rmtree(CONFIG["deploy_path"]) + os.makedirs(CONFIG["deploy_path"]) @app.command() def build() -> None: - subprocess.run(shlex.split('git submodule update --remote --merge')) + subprocess.run(shlex.split("git submodule update --remote --merge")) pelican_run() - @app.command() def watch() -> None: - - import_path = Path(CONFIG['import_path']) - content_path = Path(st.SETTINGS['PATH']) + import_path = Path(CONFIG["import_path"]) + content_path = Path(st.SETTINGS["PATH"]) def do_import(): assets = [] - for src in import_path.rglob('*'): + for src in import_path.rglob("*"): relpath = src.relative_to(import_path) target = content_path / relpath if src.is_dir(): @@ -107,15 +100,14 @@ def watch() -> None: continue print(f"{target}: importing...") src.link_to(target) - subprocess.run(shlex.split(f'git add {target}')) - uri = target.relative_to('content') + 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\t".join(["\nImported Asset URLS:"] + assets)) print("\n") - watcher = INotifyWatcher() watcher.watch(import_path, do_import) watcher.start(do_import) @@ -124,33 +116,29 @@ def watch() -> None: watcher.examine() sleep(5) - @app.command() def serve() -> None: - - url = 'http://{host}:{port}/'.format(**CONFIG) + 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}"']) - + 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'] + theme_path = st.SETTINGS["THEME"] watched_globs = [ - CONFIG['settings_base'], - '{}/templates/**/*.html'.format(theme_path), + CONFIG["settings_base"], + "{}/templates/**/*.html".format(theme_path), ] - content_file_extensions = ['.md', '.rst'] + content_file_extensions = [".md", ".rst"] for extension in content_file_extensions: - content_glob = '{0}/**/*{1}'.format(st.SETTINGS['PATH'], extension) + content_glob = "{0}/**/*{1}".format(st.SETTINGS["PATH"], extension) watched_globs.append(content_glob) - static_file_extensions = ['.css', '.js'] + static_file_extensions = [".css", ".js"] for extension in static_file_extensions: - static_file_glob = '{0}/static/**/*{1}'.format(theme_path, extension) + static_file_glob = "{0}/static/**/*{1}".format(theme_path, extension) watched_globs.append(static_file_glob) for glob in watched_globs: @@ -159,69 +147,46 @@ def serve() -> None: if st.OPEN_BROWSER_ON_SERVE: webbrowser.open(url) - server.serve(host=CONFIG['host'], port=CONFIG['port'], - root=CONFIG['deploy_path']) - + server.serve(host=CONFIG["host"], port=CONFIG["port"], root=CONFIG["deploy_path"]) @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 + 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) ) - )) - + ) @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' - ) - ], - die: Die = typer.Option( - 20, - help='The size of the die for which to create a table' - ), +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")], + die: Die = typer.Option(20, help="The size of the die for which to create a table"), template_dir: str = Annotated[ str, typer.Argument( - CONFIG['templates_path'], + CONFIG["templates_path"], help="Override the default location for markdown templates.", - ) + ), ], ) -> None: + rt = RollTable([Path(source).read_text()], frequency=frequency, die=die, hide_rolls=True) + store = rt.datasources[0].metadata["store"] - rt = RollTable( - [Path(source).read_text()], - frequency=frequency, - die=die, - hide_rolls=True - ) - store = rt.datasources[0].metadata['store'] - - click.edit(filename=create( - content_type='post', - title=store['title'], - template_dir=template_dir, - category='stores', - template='store', - extra_context=dict( - inventory=rt.as_markdown, - **store + click.edit( + filename=create( + content_type="post", + title=store["title"], + template_dir=template_dir, + category="stores", + template="store", + extra_context=dict(inventory=rt.as_markdown, **store), ) - )) - + ) @app.command() def new( @@ -242,28 +207,26 @@ def new( help="Override the default template for the content_type.", ), template_dir: str = typer.Argument( - CONFIG['templates_path'], + CONFIG["templates_path"], help="Override the default location for markdown templates.", - ) + ), ) -> None: if not category: match content_type: - case 'post': + case "post": print("You must specify a category for 'post' content.") sys.exit() - case 'monster': - category = 'bestiary' - case 'region': - category = 'locations' - case 'location': - category = 'locations' - case 'page': - category = 'pages' + case "monster": + category = "bestiary" + case "region": + category = "locations" + case "location": + category = "locations" + case "page": + category = "pages" case _: category = content_type.value - click.edit(filename=create(content_type.value, title, template_dir, - category, template or content_type.value)) - + click.edit(filename=create(content_type.value, title, template_dir, category, template or content_type.value)) def dmsh(): old_attrs = termios.tcgetattr(sys.stdin) @@ -272,6 +235,5 @@ def dmsh(): finally: termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs) - -if __name__ == '__main__': +if __name__ == "__main__": app() diff --git a/deadsands/site_tools/console.py b/deadsands/site_tools/console.py index c260e39..7e31004 100644 --- a/deadsands/site_tools/console.py +++ b/deadsands/site_tools/console.py @@ -1,38 +1,33 @@ import os - from configparser import ConfigParser from pathlib import Path from textwrap import dedent -from typing import Union, List +from typing import List, Union import rich.repr - +from prompt_toolkit import PromptSession +from prompt_toolkit.formatted_text import ANSI +from prompt_toolkit.output import ColorDepth +from prompt_toolkit.patch_stdout import patch_stdout +from prompt_toolkit.styles import Style from rich.console import Console as _Console from rich.markdown import Markdown +from rich.table import Column, Table from rich.theme import Theme -from rich.table import Table, Column - -from prompt_toolkit import PromptSession -from prompt_toolkit.patch_stdout import patch_stdout -from prompt_toolkit.formatted_text import ANSI -from prompt_toolkit.styles import Style -from prompt_toolkit.output import ColorDepth - BASE_STYLE = { - 'help': 'cyan', - 'bright': 'white', - 'repr.str': 'dim', - 'repr.brace': 'dim', - 'repr.url': 'blue', - 'table.header': 'white', - 'toolbar.fg': '#888888', - 'toolbar.bg': '#111111', - 'toolbar.bold': '#FFFFFF', - 'error': 'red', + "help": "cyan", + "bright": "white", + "repr.str": "dim", + "repr.brace": "dim", + "repr.url": "blue", + "table.header": "white", + "toolbar.fg": "#888888", + "toolbar.bg": "#111111", + "toolbar.bold": "#FFFFFF", + "error": "red", } - def console_theme(theme_name: Union[str, None] = None) -> dict: """ Return a console theme as a dictionary. @@ -41,15 +36,12 @@ def console_theme(theme_name: Union[str, None] = None) -> dict: theme_name (str): """ cfg = ConfigParser() - cfg.read_dict({'styles': BASE_STYLE}) + cfg.read_dict({"styles": BASE_STYLE}) if theme_name: - theme_path = theme_name if theme_name else os.environ.get('DEFAULT_THEME', 'blue_train') - cfg.read(Theme( - Path(theme_path) / Path('console.cfg') - )) - return cfg['styles'] - + theme_path = theme_name if theme_name else os.environ.get("DEFAULT_THEME", "blue_train") + cfg.read(Theme(Path(theme_path) / Path("console.cfg"))) + return cfg["styles"] @rich.repr.auto class Console(_Console): @@ -79,17 +71,15 @@ class Console(_Console): """ def __init__(self, *args, **kwargs): - self._console_theme = console_theme(kwargs.get('theme', None)) - self._overflow = 'ellipsis' - kwargs['theme'] = Theme(self._console_theme, inherit=False) + self._console_theme = console_theme(kwargs.get("theme", None)) + self._overflow = "ellipsis" + kwargs["theme"] = Theme(self._console_theme, inherit=False) super().__init__(*args, **kwargs) self._session = PromptSession() - @property def theme(self) -> Theme: return self._console_theme - def prompt(self, lines: List, **kwargs) -> str: """ Print a list of lines, using the final line as a prompt. @@ -102,52 +92,45 @@ class Console(_Console): """ - prompt_style = Style.from_dict({ - # 'bottom-toolbar': f"{self.theme['toolbar.fg']} bg:{self.theme['toolbar.bg']}", - # 'toolbar-bold': f"{self.theme['toolbar.bold']}" - }) + prompt_style = Style.from_dict( + { + # 'bottom-toolbar': f"{self.theme['toolbar.fg']} bg:{self.theme['toolbar.bg']}", + # 'toolbar-bold': f"{self.theme['toolbar.bold']}" + } + ) for line in lines[:-1]: super().print(line) with self.capture() as capture: - super().print(f"[prompt bold]{lines[-1]}>[/] ", end='') + super().print(f"[prompt bold]{lines[-1]}>[/] ", end="") text = ANSI(capture.get()) # This is required to intercept key bindings and not mess up the # prompt. Both the prompt and bottom_toolbar params must be functions # for this to correctly regenerate the prompt after the interrupt. with patch_stdout(raw=True): - return self._session.prompt( - lambda: text, - style=prompt_style, - color_depth=ColorDepth.TRUE_COLOR, - **kwargs) - + return self._session.prompt(lambda: text, style=prompt_style, color_depth=ColorDepth.TRUE_COLOR, **kwargs) def mdprint(self, txt: str, **kwargs) -> None: """ Like print(), but support markdown. Text will be dedented. """ - self.print(Markdown(dedent(txt), justify='left'), **kwargs) - + self.print(Markdown(dedent(txt), justify="left"), **kwargs) def print(self, txt: str, **kwargs) -> None: """ Print text to the console, possibly truncated with an ellipsis. """ super().print(txt, overflow=self._overflow, **kwargs) - def debug(self, txt: str, **kwargs) -> None: """ Print text to the console with the current theme's debug style applied, if debugging is enabled. """ - if os.environ.get('DEBUG', None): - self.print(dedent(txt), style='debug') - + if os.environ.get("DEBUG", None): + self.print(dedent(txt), style="debug") def error(self, txt: str, **kwargs) -> None: """ Print text to the console with the current theme's error style applied. """ - self.print(dedent(txt), style='error') - + self.print(dedent(txt), style="error") def table(self, *cols: List[Column], **params) -> None: """ Print a rich table to the console with theme elements and styles applied. @@ -162,10 +145,10 @@ class Console(_Console): caption_style=background_style, style=background_style, ) - params['min_width'] = 80 - width = os.environ.get('CONSOLE_WIDTH', 'auto') - if width == 'expand': - params['expand'] = True - elif width != 'auto': - params['width'] = int(width) + params["min_width"] = 80 + width = os.environ.get("CONSOLE_WIDTH", "auto") + if width == "expand": + params["expand"] = True + elif width != "auto": + params["width"] = int(width) return Table(*cols, **params) diff --git a/deadsands/site_tools/content_manager.py b/deadsands/site_tools/content_manager.py index b9ad6d4..0788922 100644 --- a/deadsands/site_tools/content_manager.py +++ b/deadsands/site_tools/content_manager.py @@ -1,48 +1,59 @@ import datetime -from jinja2 import Environment, FileSystemLoader from pathlib import Path + +from jinja2 import Environment, FileSystemLoader +from pelican.utils import sanitised_join, slugify from pelican.writers import Writer -from pelican.utils import slugify, sanitised_join + from site_tools import SETTINGS -def create(content_type: str, title: str, template_dir: str, - category: str = None, source: str = None, template: str = None, - extra_context: dict = {}) -> str: +def create( + content_type: str, + title: str, + template_dir: str, + category: str = None, + source: str = None, + template: str = None, + extra_context: dict = {}, +) -> str: """ Return the path to a new source file. """ base_path = Path.cwd() def _slugify(s): - return slugify(s, regex_subs=SETTINGS['SLUG_REGEX_SUBSTITUTIONS']) - + return slugify(s, regex_subs=SETTINGS["SLUG_REGEX_SUBSTITUTIONS"]) template_path = Path(template_dir) template_name = f"{template or content_type}.md" if not (template_path / template_name).exists(): print(f"Expected template {template_name} not found. Using default markdown template.") - template_name = 'default.md' + template_name = "default.md" env = Environment( loader=FileSystemLoader(template_path), trim_blocks=True, ) - env.add_extension('site_tools.extensions.RollTable') + env.add_extension("site_tools.extensions.RollTable") template_source = env.get_template(template_name) - target_filename = _slugify(title) + '.md' + target_filename = _slugify(title) + ".md" - relpath = Path(slugify(category)) if category else '' + relpath = Path(slugify(category)) if category else "" - target_path = base_path / SETTINGS['PATH'] / relpath + target_path = base_path / SETTINGS["PATH"] / relpath dest = sanitised_join(str(target_path / target_filename)) - SETTINGS['WRITE_SELECTED'].append(dest) + SETTINGS["WRITE_SELECTED"].append(dest) writer = Writer(target_path, settings=SETTINGS) - writer.write_file(name=target_filename, template=template_source, context={ - 'title': title, - 'tags': content_type, - 'date': datetime.datetime.now(), - 'filename': str(relpath / target_filename), - **extra_context - }) + writer.write_file( + name=target_filename, + template=template_source, + context={ + "title": title, + "tags": content_type, + "date": datetime.datetime.now(), + "filename": str(relpath / target_filename), + **extra_context, + }, + ) return dest diff --git a/deadsands/site_tools/extensions.py b/deadsands/site_tools/extensions.py index 48fd215..b112ca8 100644 --- a/deadsands/site_tools/extensions.py +++ b/deadsands/site_tools/extensions.py @@ -1,13 +1,13 @@ import textwrap -from rolltable.tables import RollTable as _RT -from jinja2_simple_tags import StandaloneTag from pathlib import Path +from jinja2_simple_tags import StandaloneTag +from rolltable.tables import RollTable as _RT + class RollTable(StandaloneTag): tags = {"rolltable"} - def render(self, sources, frequency='default', die=8, indent=0, **kwargs): - rt = _RT([Path(f'sources/{s}.yaml').read_text() for s in sources], - frequency=frequency, die=die) - return textwrap.indent(rt.as_yaml(), ' ' * indent) + def render(self, sources, frequency="default", die=8, indent=0, **kwargs): + rt = _RT([Path(f"sources/{s}.yaml").read_text() for s in sources], frequency=frequency, die=die) + return textwrap.indent(rt.as_yaml(), " " * indent) diff --git a/deadsands/site_tools/shell.py b/deadsands/site_tools/shell.py index 663e00d..e1488ce 100644 --- a/deadsands/site_tools/shell.py +++ b/deadsands/site_tools/shell.py @@ -1,14 +1,14 @@ -from prompt_toolkit import PromptSession -from prompt_toolkit.completion import NestedCompleter -from prompt_toolkit.completion import FuzzyWordCompleter -from site_tools.cli import app from enum import EnumMeta from inspect import signature + +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import FuzzyWordCompleter, NestedCompleter from rich import print +from site_tools.cli import app + def dmsh(): - session = PromptSession() def cmd2dict(cmd): @@ -16,17 +16,14 @@ def dmsh(): if not sig.parameters: return None cmds = {} - for (k, v) in list(sig.parameters.items()): + for k, v in list(sig.parameters.items()): print(v, dir(v)) if v.annotation.__class__ == EnumMeta: cmds[k] = FuzzyWordCompleter([e.value for e in v.annotation]) else: cmds[k] = None return cmds - - commands = dict( - site=dict((c.callback.__name__, cmd2dict(c.callback)) for c in app.registered_commands) - ) + commands = dict(site=dict((c.callback.__name__, cmd2dict(c.callback)) for c in app.registered_commands)) print(commands) completer = NestedCompleter.from_nested_dict(commands) text = session.prompt("DM> ", completer=completer, complete_while_typing=True, enable_history_search=False) diff --git a/deadsands/site_tools/shell/base.py b/deadsands/site_tools/shell/base.py index 1ae7a9d..75049f4 100644 --- a/deadsands/site_tools/shell/base.py +++ b/deadsands/site_tools/shell/base.py @@ -1,18 +1,17 @@ import functools -from collections import namedtuple, defaultdict +from collections import defaultdict, namedtuple +from textwrap import dedent from prompt_toolkit.completion import NestedCompleter from site_tools.console import Console -from textwrap import dedent COMMANDS = defaultdict(dict) -Command = namedtuple('Commmand', 'prompt,handler,usage,completer') - +Command = namedtuple("Commmand", "prompt,handler,usage,completer") def register_command(handler, usage, completer=None): - prompt = handler.__qualname__.split('.', -1)[0] + prompt = handler.__qualname__.split(".", -1)[0] cmd = handler.__name__ if cmd not in COMMANDS[prompt]: COMMANDS[prompt][cmd] = Command( @@ -22,7 +21,6 @@ def register_command(handler, usage, completer=None): completer=completer, ) - def command(usage, completer=None, binding=None): def decorator(func): register_command(func, usage, completer) @@ -33,49 +31,38 @@ def command(usage, completer=None, binding=None): return wrapper return decorator - class BasePrompt(NestedCompleter): - def __init__(self, cache={}): super(BasePrompt, self).__init__(self._nested_completer_map()) - self._prompt = '' + self._prompt = "" self._console = None self._theme = None self._toolbar = None self._key_bindings = None self._subshells = {} self._cache = cache - self._name = 'Interactive Shell' - + self._name = "Interactive Shell" def _register_subshells(self): for subclass in BasePrompt.__subclasses__(): if subclass.__name__ == self.__class__.__name__: continue self._subshells[subclass.__name__] = subclass(parent=self) - def _nested_completer_map(self): - return dict( - (cmd_name, cmd.completer) for (cmd_name, cmd) in COMMANDS[self.__class__.__name__].items() - ) - + return dict((cmd_name, cmd.completer) for (cmd_name, cmd) in COMMANDS[self.__class__.__name__].items()) def _get_help(self, cmd=None): try: return dedent(COMMANDS[self.__class__.__name__][cmd].usage) except KeyError: return self.usage - @property def name(self): return self._name - @property def cache(self): return self._cache - @property def key_bindings(self): return self._key_bindings - @property def usage(self): text = dedent(f""" @@ -86,56 +73,45 @@ class BasePrompt(NestedCompleter): [title]COMMANDS[/title] """) - for (name, cmd) in sorted(self.commands.items()): + for name, cmd in sorted(self.commands.items()): text += f" [b]{name:10s}[/b] {cmd.handler.__doc__.strip()}\n" return text - @property def commands(self): return COMMANDS[self.__class__.__name__] - @property def console(self): if not self._console: - self._console = Console(color_system='truecolor') + self._console = Console(color_system="truecolor") return self._console - @property def prompt(self): return self._prompt - @property def autocomplete_values(self): return list(self.commands.keys()) - @property def toolbar(self): return self._toolbar - @property def key_bindings(self): return self._key_bindings - def help(self, parts): attr = None if parts: attr = parts[0] self.console.print(self._get_help(attr)) return True - def process(self, cmd, *parts): if cmd in self.commands: return self.commands[cmd].handler(self, parts) self.console.error(f"Command {cmd} not understood; try 'help' for help.") - def start(self, cmd=None): while True: if not cmd: cmd = self.console.prompt( - self.prompt, - completer=self, - bottom_toolbar=self.toolbar, - key_bindings=self.key_bindings) + self.prompt, completer=self, bottom_toolbar=self.toolbar, key_bindings=self.key_bindings + ) if cmd: cmd, *parts = cmd.split() self.process(cmd, *parts) diff --git a/deadsands/site_tools/shell/interactive_shell.py b/deadsands/site_tools/shell/interactive_shell.py index 2486b00..f938832 100644 --- a/deadsands/site_tools/shell/interactive_shell.py +++ b/deadsands/site_tools/shell/interactive_shell.py @@ -1,47 +1,43 @@ -from site_tools.shell.base import BasePrompt, command -from rolltable.tables import RollTable -from rich.table import Table from pathlib import Path + +from prompt_toolkit.application import get_app from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.application import get_app +from rich.table import Table +from rolltable.tables import RollTable +from site_tools.shell.base import BasePrompt, command BINDINGS = KeyBindings() - class DMShell(BasePrompt): - def __init__(self, cache={}): super().__init__(cache) self._name = "DM Shell" - self._prompt = ['dm'] - self._toolbar = [('class:bold', ' DMSH ')] + self._prompt = ["dm"] + self._toolbar = [("class:bold", " DMSH ")] self._key_bindings = BINDINGS self._register_subshells() self._register_keybindings() - def _register_keybindings(self): + self._toolbar.extend( + [ + ("", " [H]elp "), + ("", " [W]ild Magic Table "), + ("", " [Q]uit "), + ] + ) - self._toolbar.extend([ - ('', " [H]elp "), - ('', " [W]ild Magic Table "), - ('', " [Q]uit "), - ]) - - @self.key_bindings.add('c-q') - @self.key_bindings.add('c-d') + @self.key_bindings.add("c-q") + @self.key_bindings.add("c-d") def quit(event): self.quit() - - @self.key_bindings.add('c-h') + @self.key_bindings.add("c-h") def help(event): self.help() - - @self.key_bindings.add('c-w') + @self.key_bindings.add("c-w") def wmt(event): self.wmt() - @command(usage=""" [title]QUIT[/title] @@ -59,7 +55,6 @@ class DMShell(BasePrompt): get_app().exit() finally: raise SystemExit("") - @command(usage=""" [title]HELP FOR THE HELP LORD[/title] @@ -76,7 +71,6 @@ class DMShell(BasePrompt): """ super().help(parts) return True - @command(usage=""" [title]INCREMENT DATE[/title] @@ -91,8 +85,8 @@ class DMShell(BasePrompt): Increment the date by one day. """ raise NotImplementedError() - - @command(usage=""" + @command( + usage=""" [title]LOCATION[/title] [b]loc[/b] sets the party's location to the specified region of the Sahwat Desert. @@ -101,20 +95,15 @@ class DMShell(BasePrompt): [link]loc LOCATION[/link] """, - completer=WordCompleter([ - "The Blooming Wastes", - "Dust River Canyon", - "Gopher Gulch", - "Calamity Ridge" - ])) + completer=WordCompleter(["The Blooming Wastes", "Dust River Canyon", "Gopher Gulch", "Calamity Ridge"]), + ) def loc(self, parts=[]): """ Move the party to a new region of the Sahwat Desert. """ if parts: - self.cache['location'] = (' '.join(parts)) + self.cache["location"] = " ".join(parts) self.console.print(f"The party is in {self.cache['location']}.") - @command(usage=""" [title]OVERLAND TRAVEL[/title] @@ -129,7 +118,6 @@ class DMShell(BasePrompt): Increment the date by one day and record """ raise NotImplementedError() - @command(usage=""" [title]WILD MAGIC TABLE[/title] @@ -145,18 +133,18 @@ class DMShell(BasePrompt): sources/sahwat_magic_table.yaml \\ --frequency default --die 20[/link] """) - def wmt(self, *parts, source='sahwat_magic_table.yaml'): + def wmt(self, *parts, source="sahwat_magic_table.yaml"): """ Generate a Wild Magic Table for resolving spell effects. """ - if 'wmt' not in self.cache: + if "wmt" not in self.cache: rt = RollTable( [Path(f"{self.cache['table_sources_path']}/{source}").read_text()], - frequency='default', + frequency="default", die=20, ) table = Table(*rt.expanded_rows[0]) for row in rt.expanded_rows[1:]: table.add_row(*row) - self.cache['wmt'] = table - self.console.print(self.cache['wmt']) + self.cache["wmt"] = table + self.console.print(self.cache["wmt"])