reformatting

This commit is contained in:
evilchili 2023-07-04 10:59:51 -07:00
parent 44fc415100
commit 6ad0ab68da
9 changed files with 254 additions and 314 deletions

View File

@ -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] [build-system]
requires = ['poetry-core~=1.0'] requires = ['poetry-core~=1.0']
build-backend = 'poetry.core.masonry.api' 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

View File

@ -2,8 +2,8 @@ from pelican.settings import DEFAULT_CONFIG, get_settings_from_file
OPEN_BROWSER_ON_SERVE = True OPEN_BROWSER_ON_SERVE = True
DEV_SETTINGS_FILE_BASE = 'pelicanconf.py' DEV_SETTINGS_FILE_BASE = "pelicanconf.py"
PUB_SETTINGS_FILE_BASE = 'publishconf.py' PUB_SETTINGS_FILE_BASE = "publishconf.py"
SETTINGS = {} SETTINGS = {}

View File

@ -1,102 +1,95 @@
import asyncio import asyncio
import click
import os import os
import shlex
import shutil import shutil
import subprocess import subprocess
import shlex
import sys import sys
import typer
import webbrowser
import termios import termios
import webbrowser
import site_tools as st from collections import defaultdict
from enum import Enum from enum import Enum
from pathlib import Path
from time import sleep
import click
import typer
from livereload import Server from livereload import Server
from livereload.watcher import INotifyWatcher from livereload.watcher import INotifyWatcher
from pathlib import Path
from pelican import main as pelican_main from pelican import main as pelican_main
from time import sleep from rolltable.tables import RollTable
from typing_extensions import Annotated from typing_extensions import Annotated
from collections import defaultdict
import site_tools as st
from site_tools.content_manager import create from site_tools.content_manager import create
from site_tools.shell.interactive_shell import DMShell from site_tools.shell.interactive_shell import DMShell
from rolltable.tables import RollTable
CONFIG = defaultdict(dict) CONFIG = defaultdict(dict)
CONFIG.update({ CONFIG.update(
'settings_base': st.DEV_SETTINGS_FILE_BASE, {
'settings_publish': st.PUB_SETTINGS_FILE_BASE, "settings_base": st.DEV_SETTINGS_FILE_BASE,
# Output path. Can be absolute or relative to tasks.py. Default: 'output' "settings_publish": st.PUB_SETTINGS_FILE_BASE,
'deploy_path': st.SETTINGS['OUTPUT_PATH'], # Output path. Can be absolute or relative to tasks.py. Default: 'output'
# Remote server configuration "deploy_path": st.SETTINGS["OUTPUT_PATH"],
'ssh_user': 'greg', # Remote server configuration
'ssh_host': 'froghat.club', "ssh_user": "greg",
'ssh_port': '22', "ssh_host": "froghat.club",
'ssh_path': '/usr/local/deploy/deadsands/', "ssh_port": "22",
# Host and port for `serve` "ssh_path": "/usr/local/deploy/deadsands/",
'host': 'localhost', # Host and port for `serve`
'port': 8000, "host": "localhost",
# content manager config "port": 8000,
'templates_path': 'markdown-templates', # content manager config
# directory to watch for new assets "templates_path": "markdown-templates",
'import_path': 'imports', # directory to watch for new assets
# where new asseets will be made available "import_path": "imports",
'production_host': 'deadsands.froghat.club', # where new asseets will be made available
# where to find roll table sources "production_host": "deadsands.froghat.club",
'table_sources_path': 'sources', # where to find roll table sources
}) "table_sources_path": "sources",
}
)
app = typer.Typer() app = typer.Typer()
class ContentType(str, Enum): class ContentType(str, Enum):
post = 'post' post = "post"
lore = 'lore' lore = "lore"
monster = 'monster' monster = "monster"
region = 'region' region = "region"
location = 'location' location = "location"
page = 'page' page = "page"
class Die(str, Enum): class Die(str, Enum):
d100 = '100' d100 = "100"
d20 = '20' d20 = "20"
d12 = '12' d12 = "12"
d10 = '10' d10 = "10"
d6 = '6' d6 = "6"
d4 = '4' d4 = "4"
def pelican_run(cmd: list = [], publish=False) -> None: def pelican_run(cmd: list = [], publish=False) -> None:
settings = CONFIG['settings_publish' if publish else 'settings_base'] settings = CONFIG["settings_publish" if publish else "settings_base"]
pelican_main(['-s', settings] + cmd) pelican_main(["-s", settings] + cmd)
@app.command() @app.command()
def clean() -> None: def clean() -> None:
if os.path.isdir(CONFIG['deploy_path']): if os.path.isdir(CONFIG["deploy_path"]):
shutil.rmtree(CONFIG['deploy_path']) shutil.rmtree(CONFIG["deploy_path"])
os.makedirs(CONFIG['deploy_path']) os.makedirs(CONFIG["deploy_path"])
@app.command() @app.command()
def build() -> None: def build() -> None:
subprocess.run(shlex.split('git submodule update --remote --merge')) subprocess.run(shlex.split("git submodule update --remote --merge"))
pelican_run() pelican_run()
@app.command() @app.command()
def watch() -> None: def watch() -> None:
import_path = Path(CONFIG["import_path"])
import_path = Path(CONFIG['import_path']) content_path = Path(st.SETTINGS["PATH"])
content_path = Path(st.SETTINGS['PATH'])
def do_import(): def do_import():
assets = [] assets = []
for src in import_path.rglob('*'): for src in import_path.rglob("*"):
relpath = src.relative_to(import_path) relpath = src.relative_to(import_path)
target = content_path / relpath target = content_path / relpath
if src.is_dir(): if src.is_dir():
@ -107,15 +100,14 @@ def watch() -> None:
continue continue
print(f"{target}: importing...") print(f"{target}: importing...")
src.link_to(target) src.link_to(target)
subprocess.run(shlex.split(f'git add {target}')) subprocess.run(shlex.split(f"git add {target}"))
uri = target.relative_to('content') uri = target.relative_to("content")
assets.append(f"https://{CONFIG['production_host']}/{uri}") assets.append(f"https://{CONFIG['production_host']}/{uri}")
src.unlink() src.unlink()
if assets: if assets:
publish() publish()
print('\n\t'.join(["\nImported Asset URLS:"] + assets)) print("\n\t".join(["\nImported Asset URLS:"] + assets))
print("\n") print("\n")
watcher = INotifyWatcher() watcher = INotifyWatcher()
watcher.watch(import_path, do_import) watcher.watch(import_path, do_import)
watcher.start(do_import) watcher.start(do_import)
@ -124,33 +116,29 @@ def watch() -> None:
watcher.examine() watcher.examine()
sleep(5) sleep(5)
@app.command() @app.command()
def serve() -> None: def serve() -> None:
url = "http://{host}:{port}/".format(**CONFIG)
url = 'http://{host}:{port}/'.format(**CONFIG)
def cached_build(): def cached_build():
pelican_run(['-ve', 'CACHE_CONTENT=true', 'LOAD_CONTENT_CACHE=true', pelican_run(["-ve", "CACHE_CONTENT=true", "LOAD_CONTENT_CACHE=true", "SHOW_DRAFTS=true", f'SITEURL="{url}"'])
'SHOW_DRAFTS=true', f'SITEURL="{url}"'])
clean() clean()
cached_build() cached_build()
server = Server() server = Server()
theme_path = st.SETTINGS['THEME'] theme_path = st.SETTINGS["THEME"]
watched_globs = [ watched_globs = [
CONFIG['settings_base'], CONFIG["settings_base"],
'{}/templates/**/*.html'.format(theme_path), "{}/templates/**/*.html".format(theme_path),
] ]
content_file_extensions = ['.md', '.rst'] content_file_extensions = [".md", ".rst"]
for extension in content_file_extensions: 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) watched_globs.append(content_glob)
static_file_extensions = ['.css', '.js'] static_file_extensions = [".css", ".js"]
for extension in static_file_extensions: 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) watched_globs.append(static_file_glob)
for glob in watched_globs: for glob in watched_globs:
@ -159,69 +147,46 @@ def serve() -> None:
if st.OPEN_BROWSER_ON_SERVE: if st.OPEN_BROWSER_ON_SERVE:
webbrowser.open(url) webbrowser.open(url)
server.serve(host=CONFIG['host'], port=CONFIG['port'], server.serve(host=CONFIG["host"], port=CONFIG["port"], root=CONFIG["deploy_path"])
root=CONFIG['deploy_path'])
@app.command() @app.command()
def publish() -> None: def publish() -> None:
clean() clean()
pelican_run(publish=True) pelican_run(publish=True)
subprocess.run(shlex.split( subprocess.run(
'rsync --delete --exclude ".DS_Store" -pthrvz -c ' shlex.split(
'-e "ssh -p {ssh_port}" ' 'rsync --delete --exclude ".DS_Store" -pthrvz -c '
'{} {ssh_user}@{ssh_host}:{ssh_path}'.format( '-e "ssh -p {ssh_port}" '
CONFIG['deploy_path'].rstrip('/') + '/', "{} {ssh_user}@{ssh_host}:{ssh_path}".format(CONFIG["deploy_path"].rstrip("/") + "/", **CONFIG)
**CONFIG
) )
)) )
@app.command() @app.command()
def restock(source: str = typer.Argument( def restock(
..., source: str = typer.Argument(..., help="The source file for the store."),
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"),
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[ template_dir: str = Annotated[
str, str,
typer.Argument( typer.Argument(
CONFIG['templates_path'], CONFIG["templates_path"],
help="Override the default location for markdown templates.", help="Override the default location for markdown templates.",
) ),
], ],
) -> None: ) -> None:
rt = RollTable([Path(source).read_text()], frequency=frequency, die=die, hide_rolls=True)
store = rt.datasources[0].metadata["store"]
rt = RollTable( click.edit(
[Path(source).read_text()], filename=create(
frequency=frequency, content_type="post",
die=die, title=store["title"],
hide_rolls=True template_dir=template_dir,
) category="stores",
store = rt.datasources[0].metadata['store'] 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() @app.command()
def new( def new(
@ -242,28 +207,26 @@ def new(
help="Override the default template for the content_type.", help="Override the default template for the content_type.",
), ),
template_dir: str = typer.Argument( template_dir: str = typer.Argument(
CONFIG['templates_path'], CONFIG["templates_path"],
help="Override the default location for markdown templates.", help="Override the default location for markdown templates.",
) ),
) -> None: ) -> None:
if not category: if not category:
match content_type: match content_type:
case 'post': case "post":
print("You must specify a category for 'post' content.") print("You must specify a category for 'post' content.")
sys.exit() sys.exit()
case 'monster': case "monster":
category = 'bestiary' category = "bestiary"
case 'region': case "region":
category = 'locations' category = "locations"
case 'location': case "location":
category = 'locations' category = "locations"
case 'page': case "page":
category = 'pages' category = "pages"
case _: case _:
category = content_type.value category = content_type.value
click.edit(filename=create(content_type.value, title, template_dir, click.edit(filename=create(content_type.value, title, template_dir, category, template or content_type.value))
category, template or content_type.value))
def dmsh(): def dmsh():
old_attrs = termios.tcgetattr(sys.stdin) old_attrs = termios.tcgetattr(sys.stdin)
@ -272,6 +235,5 @@ def dmsh():
finally: finally:
termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs) termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs)
if __name__ == "__main__":
if __name__ == '__main__':
app() app()

View File

@ -1,38 +1,33 @@
import os import os
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from textwrap import dedent from textwrap import dedent
from typing import Union, List from typing import List, Union
import rich.repr 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.console import Console as _Console
from rich.markdown import Markdown from rich.markdown import Markdown
from rich.table import Column, Table
from rich.theme import Theme 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 = { BASE_STYLE = {
'help': 'cyan', "help": "cyan",
'bright': 'white', "bright": "white",
'repr.str': 'dim', "repr.str": "dim",
'repr.brace': 'dim', "repr.brace": "dim",
'repr.url': 'blue', "repr.url": "blue",
'table.header': 'white', "table.header": "white",
'toolbar.fg': '#888888', "toolbar.fg": "#888888",
'toolbar.bg': '#111111', "toolbar.bg": "#111111",
'toolbar.bold': '#FFFFFF', "toolbar.bold": "#FFFFFF",
'error': 'red', "error": "red",
} }
def console_theme(theme_name: Union[str, None] = None) -> dict: def console_theme(theme_name: Union[str, None] = None) -> dict:
""" """
Return a console theme as a dictionary. Return a console theme as a dictionary.
@ -41,15 +36,12 @@ def console_theme(theme_name: Union[str, None] = None) -> dict:
theme_name (str): theme_name (str):
""" """
cfg = ConfigParser() cfg = ConfigParser()
cfg.read_dict({'styles': BASE_STYLE}) cfg.read_dict({"styles": BASE_STYLE})
if theme_name: if theme_name:
theme_path = theme_name if theme_name else os.environ.get('DEFAULT_THEME', 'blue_train') theme_path = theme_name if theme_name else os.environ.get("DEFAULT_THEME", "blue_train")
cfg.read(Theme( cfg.read(Theme(Path(theme_path) / Path("console.cfg")))
Path(theme_path) / Path('console.cfg') return cfg["styles"]
))
return cfg['styles']
@rich.repr.auto @rich.repr.auto
class Console(_Console): class Console(_Console):
@ -79,17 +71,15 @@ class Console(_Console):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._console_theme = console_theme(kwargs.get('theme', None)) self._console_theme = console_theme(kwargs.get("theme", None))
self._overflow = 'ellipsis' self._overflow = "ellipsis"
kwargs['theme'] = Theme(self._console_theme, inherit=False) kwargs["theme"] = Theme(self._console_theme, inherit=False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._session = PromptSession() self._session = PromptSession()
@property @property
def theme(self) -> Theme: def theme(self) -> Theme:
return self._console_theme return self._console_theme
def prompt(self, lines: List, **kwargs) -> str: def prompt(self, lines: List, **kwargs) -> str:
""" """
Print a list of lines, using the final line as a prompt. Print a list of lines, using the final line as a prompt.
@ -102,52 +92,45 @@ class Console(_Console):
""" """
prompt_style = Style.from_dict({ prompt_style = Style.from_dict(
# 'bottom-toolbar': f"{self.theme['toolbar.fg']} bg:{self.theme['toolbar.bg']}", {
# 'toolbar-bold': f"{self.theme['toolbar.bold']}" # 'bottom-toolbar': f"{self.theme['toolbar.fg']} bg:{self.theme['toolbar.bg']}",
}) # 'toolbar-bold': f"{self.theme['toolbar.bold']}"
}
)
for line in lines[:-1]: for line in lines[:-1]:
super().print(line) super().print(line)
with self.capture() as capture: 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()) text = ANSI(capture.get())
# This is required to intercept key bindings and not mess up the # This is required to intercept key bindings and not mess up the
# prompt. Both the prompt and bottom_toolbar params must be functions # prompt. Both the prompt and bottom_toolbar params must be functions
# for this to correctly regenerate the prompt after the interrupt. # for this to correctly regenerate the prompt after the interrupt.
with patch_stdout(raw=True): with patch_stdout(raw=True):
return self._session.prompt( return self._session.prompt(lambda: text, style=prompt_style, color_depth=ColorDepth.TRUE_COLOR, **kwargs)
lambda: text,
style=prompt_style,
color_depth=ColorDepth.TRUE_COLOR,
**kwargs)
def mdprint(self, txt: str, **kwargs) -> None: def mdprint(self, txt: str, **kwargs) -> None:
""" """
Like print(), but support markdown. Text will be dedented. 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: def print(self, txt: str, **kwargs) -> None:
""" """
Print text to the console, possibly truncated with an ellipsis. Print text to the console, possibly truncated with an ellipsis.
""" """
super().print(txt, overflow=self._overflow, **kwargs) super().print(txt, overflow=self._overflow, **kwargs)
def debug(self, txt: str, **kwargs) -> None: def debug(self, txt: str, **kwargs) -> None:
""" """
Print text to the console with the current theme's debug style applied, if debugging is enabled. Print text to the console with the current theme's debug style applied, if debugging is enabled.
""" """
if os.environ.get('DEBUG', None): if os.environ.get("DEBUG", None):
self.print(dedent(txt), style='debug') self.print(dedent(txt), style="debug")
def error(self, txt: str, **kwargs) -> None: def error(self, txt: str, **kwargs) -> None:
""" """
Print text to the console with the current theme's error style applied. 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: def table(self, *cols: List[Column], **params) -> None:
""" """
Print a rich table to the console with theme elements and styles applied. Print a rich table to the console with theme elements and styles applied.
@ -162,10 +145,10 @@ class Console(_Console):
caption_style=background_style, caption_style=background_style,
style=background_style, style=background_style,
) )
params['min_width'] = 80 params["min_width"] = 80
width = os.environ.get('CONSOLE_WIDTH', 'auto') width = os.environ.get("CONSOLE_WIDTH", "auto")
if width == 'expand': if width == "expand":
params['expand'] = True params["expand"] = True
elif width != 'auto': elif width != "auto":
params['width'] = int(width) params["width"] = int(width)
return Table(*cols, **params) return Table(*cols, **params)

View File

@ -1,48 +1,59 @@
import datetime import datetime
from jinja2 import Environment, FileSystemLoader
from pathlib import Path from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from pelican.utils import sanitised_join, slugify
from pelican.writers import Writer from pelican.writers import Writer
from pelican.utils import slugify, sanitised_join
from site_tools import SETTINGS from site_tools import SETTINGS
def create(content_type: str, title: str, template_dir: str, def create(
category: str = None, source: str = None, template: str = None, content_type: str,
extra_context: dict = {}) -> 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. Return the path to a new source file.
""" """
base_path = Path.cwd() base_path = Path.cwd()
def _slugify(s): 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_path = Path(template_dir)
template_name = f"{template or content_type}.md" template_name = f"{template or content_type}.md"
if not (template_path / template_name).exists(): if not (template_path / template_name).exists():
print(f"Expected template {template_name} not found. Using default markdown template.") print(f"Expected template {template_name} not found. Using default markdown template.")
template_name = 'default.md' template_name = "default.md"
env = Environment( env = Environment(
loader=FileSystemLoader(template_path), loader=FileSystemLoader(template_path),
trim_blocks=True, trim_blocks=True,
) )
env.add_extension('site_tools.extensions.RollTable') env.add_extension("site_tools.extensions.RollTable")
template_source = env.get_template(template_name) 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)) 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 = Writer(target_path, settings=SETTINGS)
writer.write_file(name=target_filename, template=template_source, context={ writer.write_file(
'title': title, name=target_filename,
'tags': content_type, template=template_source,
'date': datetime.datetime.now(), context={
'filename': str(relpath / target_filename), "title": title,
**extra_context "tags": content_type,
}) "date": datetime.datetime.now(),
"filename": str(relpath / target_filename),
**extra_context,
},
)
return dest return dest

View File

@ -1,13 +1,13 @@
import textwrap import textwrap
from rolltable.tables import RollTable as _RT
from jinja2_simple_tags import StandaloneTag
from pathlib import Path from pathlib import Path
from jinja2_simple_tags import StandaloneTag
from rolltable.tables import RollTable as _RT
class RollTable(StandaloneTag): class RollTable(StandaloneTag):
tags = {"rolltable"} tags = {"rolltable"}
def render(self, sources, frequency='default', die=8, indent=0, **kwargs): def render(self, sources, frequency="default", die=8, indent=0, **kwargs):
rt = _RT([Path(f'sources/{s}.yaml').read_text() for s in sources], rt = _RT([Path(f"sources/{s}.yaml").read_text() for s in sources], frequency=frequency, die=die)
frequency=frequency, die=die) return textwrap.indent(rt.as_yaml(), " " * indent)
return textwrap.indent(rt.as_yaml(), ' ' * indent)

View File

@ -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 enum import EnumMeta
from inspect import signature from inspect import signature
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import FuzzyWordCompleter, NestedCompleter
from rich import print from rich import print
from site_tools.cli import app
def dmsh(): def dmsh():
session = PromptSession() session = PromptSession()
def cmd2dict(cmd): def cmd2dict(cmd):
@ -16,17 +16,14 @@ def dmsh():
if not sig.parameters: if not sig.parameters:
return None return None
cmds = {} cmds = {}
for (k, v) in list(sig.parameters.items()): for k, v in list(sig.parameters.items()):
print(v, dir(v)) print(v, dir(v))
if v.annotation.__class__ == EnumMeta: if v.annotation.__class__ == EnumMeta:
cmds[k] = FuzzyWordCompleter([e.value for e in v.annotation]) cmds[k] = FuzzyWordCompleter([e.value for e in v.annotation])
else: else:
cmds[k] = None cmds[k] = None
return cmds 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) print(commands)
completer = NestedCompleter.from_nested_dict(commands) completer = NestedCompleter.from_nested_dict(commands)
text = session.prompt("DM> ", completer=completer, complete_while_typing=True, enable_history_search=False) text = session.prompt("DM> ", completer=completer, complete_while_typing=True, enable_history_search=False)

View File

@ -1,18 +1,17 @@
import functools import functools
from collections import namedtuple, defaultdict from collections import defaultdict, namedtuple
from textwrap import dedent
from prompt_toolkit.completion import NestedCompleter from prompt_toolkit.completion import NestedCompleter
from site_tools.console import Console from site_tools.console import Console
from textwrap import dedent
COMMANDS = defaultdict(dict) COMMANDS = defaultdict(dict)
Command = namedtuple('Commmand', 'prompt,handler,usage,completer') Command = namedtuple("Commmand", "prompt,handler,usage,completer")
def register_command(handler, usage, completer=None): def register_command(handler, usage, completer=None):
prompt = handler.__qualname__.split('.', -1)[0] prompt = handler.__qualname__.split(".", -1)[0]
cmd = handler.__name__ cmd = handler.__name__
if cmd not in COMMANDS[prompt]: if cmd not in COMMANDS[prompt]:
COMMANDS[prompt][cmd] = Command( COMMANDS[prompt][cmd] = Command(
@ -22,7 +21,6 @@ def register_command(handler, usage, completer=None):
completer=completer, completer=completer,
) )
def command(usage, completer=None, binding=None): def command(usage, completer=None, binding=None):
def decorator(func): def decorator(func):
register_command(func, usage, completer) register_command(func, usage, completer)
@ -33,49 +31,38 @@ def command(usage, completer=None, binding=None):
return wrapper return wrapper
return decorator return decorator
class BasePrompt(NestedCompleter): class BasePrompt(NestedCompleter):
def __init__(self, cache={}): def __init__(self, cache={}):
super(BasePrompt, self).__init__(self._nested_completer_map()) super(BasePrompt, self).__init__(self._nested_completer_map())
self._prompt = '' self._prompt = ""
self._console = None self._console = None
self._theme = None self._theme = None
self._toolbar = None self._toolbar = None
self._key_bindings = None self._key_bindings = None
self._subshells = {} self._subshells = {}
self._cache = cache self._cache = cache
self._name = 'Interactive Shell' self._name = "Interactive Shell"
def _register_subshells(self): def _register_subshells(self):
for subclass in BasePrompt.__subclasses__(): for subclass in BasePrompt.__subclasses__():
if subclass.__name__ == self.__class__.__name__: if subclass.__name__ == self.__class__.__name__:
continue continue
self._subshells[subclass.__name__] = subclass(parent=self) self._subshells[subclass.__name__] = subclass(parent=self)
def _nested_completer_map(self): def _nested_completer_map(self):
return dict( return dict((cmd_name, cmd.completer) for (cmd_name, cmd) in COMMANDS[self.__class__.__name__].items())
(cmd_name, cmd.completer) for (cmd_name, cmd) in COMMANDS[self.__class__.__name__].items()
)
def _get_help(self, cmd=None): def _get_help(self, cmd=None):
try: try:
return dedent(COMMANDS[self.__class__.__name__][cmd].usage) return dedent(COMMANDS[self.__class__.__name__][cmd].usage)
except KeyError: except KeyError:
return self.usage return self.usage
@property @property
def name(self): def name(self):
return self._name return self._name
@property @property
def cache(self): def cache(self):
return self._cache return self._cache
@property @property
def key_bindings(self): def key_bindings(self):
return self._key_bindings return self._key_bindings
@property @property
def usage(self): def usage(self):
text = dedent(f""" text = dedent(f"""
@ -86,56 +73,45 @@ class BasePrompt(NestedCompleter):
[title]COMMANDS[/title] [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" text += f" [b]{name:10s}[/b] {cmd.handler.__doc__.strip()}\n"
return text return text
@property @property
def commands(self): def commands(self):
return COMMANDS[self.__class__.__name__] return COMMANDS[self.__class__.__name__]
@property @property
def console(self): def console(self):
if not self._console: if not self._console:
self._console = Console(color_system='truecolor') self._console = Console(color_system="truecolor")
return self._console return self._console
@property @property
def prompt(self): def prompt(self):
return self._prompt return self._prompt
@property @property
def autocomplete_values(self): def autocomplete_values(self):
return list(self.commands.keys()) return list(self.commands.keys())
@property @property
def toolbar(self): def toolbar(self):
return self._toolbar return self._toolbar
@property @property
def key_bindings(self): def key_bindings(self):
return self._key_bindings return self._key_bindings
def help(self, parts): def help(self, parts):
attr = None attr = None
if parts: if parts:
attr = parts[0] attr = parts[0]
self.console.print(self._get_help(attr)) self.console.print(self._get_help(attr))
return True return True
def process(self, cmd, *parts): def process(self, cmd, *parts):
if cmd in self.commands: if cmd in self.commands:
return self.commands[cmd].handler(self, parts) return self.commands[cmd].handler(self, parts)
self.console.error(f"Command {cmd} not understood; try 'help' for help.") self.console.error(f"Command {cmd} not understood; try 'help' for help.")
def start(self, cmd=None): def start(self, cmd=None):
while True: while True:
if not cmd: if not cmd:
cmd = self.console.prompt( cmd = self.console.prompt(
self.prompt, self.prompt, completer=self, bottom_toolbar=self.toolbar, key_bindings=self.key_bindings
completer=self, )
bottom_toolbar=self.toolbar,
key_bindings=self.key_bindings)
if cmd: if cmd:
cmd, *parts = cmd.split() cmd, *parts = cmd.split()
self.process(cmd, *parts) self.process(cmd, *parts)

View File

@ -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 pathlib import Path
from prompt_toolkit.application import get_app
from prompt_toolkit.completion import WordCompleter from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.key_binding import KeyBindings 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() BINDINGS = KeyBindings()
class DMShell(BasePrompt): class DMShell(BasePrompt):
def __init__(self, cache={}): def __init__(self, cache={}):
super().__init__(cache) super().__init__(cache)
self._name = "DM Shell" self._name = "DM Shell"
self._prompt = ['dm'] self._prompt = ["dm"]
self._toolbar = [('class:bold', ' DMSH ')] self._toolbar = [("class:bold", " DMSH ")]
self._key_bindings = BINDINGS self._key_bindings = BINDINGS
self._register_subshells() self._register_subshells()
self._register_keybindings() self._register_keybindings()
def _register_keybindings(self): def _register_keybindings(self):
self._toolbar.extend(
[
("", " [H]elp "),
("", " [W]ild Magic Table "),
("", " [Q]uit "),
]
)
self._toolbar.extend([ @self.key_bindings.add("c-q")
('', " [H]elp "), @self.key_bindings.add("c-d")
('', " [W]ild Magic Table "),
('', " [Q]uit "),
])
@self.key_bindings.add('c-q')
@self.key_bindings.add('c-d')
def quit(event): def quit(event):
self.quit() self.quit()
@self.key_bindings.add("c-h")
@self.key_bindings.add('c-h')
def help(event): def help(event):
self.help() self.help()
@self.key_bindings.add("c-w")
@self.key_bindings.add('c-w')
def wmt(event): def wmt(event):
self.wmt() self.wmt()
@command(usage=""" @command(usage="""
[title]QUIT[/title] [title]QUIT[/title]
@ -59,7 +55,6 @@ class DMShell(BasePrompt):
get_app().exit() get_app().exit()
finally: finally:
raise SystemExit("") raise SystemExit("")
@command(usage=""" @command(usage="""
[title]HELP FOR THE HELP LORD[/title] [title]HELP FOR THE HELP LORD[/title]
@ -76,7 +71,6 @@ class DMShell(BasePrompt):
""" """
super().help(parts) super().help(parts)
return True return True
@command(usage=""" @command(usage="""
[title]INCREMENT DATE[/title] [title]INCREMENT DATE[/title]
@ -91,8 +85,8 @@ class DMShell(BasePrompt):
Increment the date by one day. Increment the date by one day.
""" """
raise NotImplementedError() raise NotImplementedError()
@command(
@command(usage=""" usage="""
[title]LOCATION[/title] [title]LOCATION[/title]
[b]loc[/b] sets the party's location to the specified region of the Sahwat Desert. [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] [link]loc LOCATION[/link]
""", """,
completer=WordCompleter([ completer=WordCompleter(["The Blooming Wastes", "Dust River Canyon", "Gopher Gulch", "Calamity Ridge"]),
"The Blooming Wastes", )
"Dust River Canyon",
"Gopher Gulch",
"Calamity Ridge"
]))
def loc(self, parts=[]): def loc(self, parts=[]):
""" """
Move the party to a new region of the Sahwat Desert. Move the party to a new region of the Sahwat Desert.
""" """
if parts: if parts:
self.cache['location'] = (' '.join(parts)) self.cache["location"] = " ".join(parts)
self.console.print(f"The party is in {self.cache['location']}.") self.console.print(f"The party is in {self.cache['location']}.")
@command(usage=""" @command(usage="""
[title]OVERLAND TRAVEL[/title] [title]OVERLAND TRAVEL[/title]
@ -129,7 +118,6 @@ class DMShell(BasePrompt):
Increment the date by one day and record Increment the date by one day and record
""" """
raise NotImplementedError() raise NotImplementedError()
@command(usage=""" @command(usage="""
[title]WILD MAGIC TABLE[/title] [title]WILD MAGIC TABLE[/title]
@ -145,18 +133,18 @@ class DMShell(BasePrompt):
sources/sahwat_magic_table.yaml \\ sources/sahwat_magic_table.yaml \\
--frequency default --die 20[/link] --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. Generate a Wild Magic Table for resolving spell effects.
""" """
if 'wmt' not in self.cache: if "wmt" not in self.cache:
rt = RollTable( rt = RollTable(
[Path(f"{self.cache['table_sources_path']}/{source}").read_text()], [Path(f"{self.cache['table_sources_path']}/{source}").read_text()],
frequency='default', frequency="default",
die=20, die=20,
) )
table = Table(*rt.expanded_rows[0]) table = Table(*rt.expanded_rows[0])
for row in rt.expanded_rows[1:]: for row in rt.expanded_rows[1:]:
table.add_row(*row) table.add_row(*row)
self.cache['wmt'] = table self.cache["wmt"] = table
self.console.print(self.cache['wmt']) self.console.print(self.cache["wmt"])