diff --git a/deadsands/pyproject.toml b/deadsands/pyproject.toml index 698bc94..53cb2ec 100644 --- a/deadsands/pyproject.toml +++ b/deadsands/pyproject.toml @@ -36,6 +36,7 @@ prompt-toolkit = "^3.0.38" site = "site_tools.cli:app" roll-table = "rolltable.cli:app" pelican = "site_tools.tasks:pelican_main" +dmsh = "site_tools.cli:dmsh" diff --git a/deadsands/site_tools/cli.py b/deadsands/site_tools/cli.py index 1a29621..84d6112 100644 --- a/deadsands/site_tools/cli.py +++ b/deadsands/site_tools/cli.py @@ -7,6 +7,7 @@ import shlex import sys import typer import webbrowser +import termios import site_tools as st @@ -20,11 +21,12 @@ from typing_extensions import Annotated from collections import defaultdict from site_tools.content_manager import create -from site_tools.shell.interactive_shell import InteractiveShell +from site_tools.shell.interactive_shell import DMShell from rolltable.tables import RollTable -CONFIG = { +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' @@ -45,7 +47,7 @@ CONFIG = { 'production_host': 'deadsands.froghat.club', # where to find roll table sources 'table_sources_path': 'sources', -} +}) app = typer.Typer() @@ -263,22 +265,10 @@ def new( category, template or content_type.value)) -@app.command() def dmsh(): - import termios, sys - - session = defaultdict(dict) - prompt = InteractiveShell( - [ - "[title]DM's Shell.[/title]", - 'dmsh' - ], config=CONFIG, session=session - ) - - # ensure the terminal is restored on exit. old_attrs = termios.tcgetattr(sys.stdin) try: - asyncio.run(prompt.start()) + asyncio.run(DMShell(CONFIG).start()) finally: termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs) diff --git a/deadsands/site_tools/console.py b/deadsands/site_tools/console.py index 4771c55..c260e39 100644 --- a/deadsands/site_tools/console.py +++ b/deadsands/site_tools/console.py @@ -29,6 +29,7 @@ BASE_STYLE = { 'toolbar.fg': '#888888', 'toolbar.bg': '#111111', 'toolbar.bold': '#FFFFFF', + 'error': 'red', } diff --git a/deadsands/site_tools/shell/base.py b/deadsands/site_tools/shell/base.py index 009208f..1ae7a9d 100644 --- a/deadsands/site_tools/shell/base.py +++ b/deadsands/site_tools/shell/base.py @@ -3,7 +3,6 @@ from collections import namedtuple, defaultdict from prompt_toolkit.completion import NestedCompleter - from site_tools.console import Console from textwrap import dedent @@ -37,13 +36,22 @@ def command(usage, completer=None, binding=None): class BasePrompt(NestedCompleter): - def __init__(self, console=None): + def __init__(self, cache={}): super(BasePrompt, self).__init__(self._nested_completer_map()) - self._prompt = '' - self._autocomplete_values = [] self._console = None self._theme = None + self._toolbar = None + self._key_bindings = None + self._subshells = {} + self._cache = cache + 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( @@ -56,13 +64,22 @@ class BasePrompt(NestedCompleter): except KeyError: return self.usage - def default_completer(self, document, complete_event): - raise NotImplementedError(f"Implement the 'default_completer' method of {self.__class__.__name__}") + @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(""" - [title]dmsh[/title] + text = dedent(f""" + [title]{self.name}[/title] Available commands are listed below. Try 'help COMMAND' for detailed help. @@ -89,15 +106,15 @@ class BasePrompt(NestedCompleter): @property def autocomplete_values(self): - return self._autocomplete_values + return list(self.commands.keys()) @property def toolbar(self): - return None + return self._toolbar @property def key_bindings(self): - return None + return self._key_bindings def help(self, parts): attr = None @@ -109,7 +126,7 @@ class BasePrompt(NestedCompleter): 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.") + self.console.error(f"Command {cmd} not understood; try 'help' for help.") def start(self, cmd=None): while True: diff --git a/deadsands/site_tools/shell/interactive_shell.py b/deadsands/site_tools/shell/interactive_shell.py index a37a66d..2486b00 100644 --- a/deadsands/site_tools/shell/interactive_shell.py +++ b/deadsands/site_tools/shell/interactive_shell.py @@ -7,72 +7,41 @@ from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.application import get_app -bindings = KeyBindings() +BINDINGS = KeyBindings() -class InteractiveShell(BasePrompt): +class DMShell(BasePrompt): - def __init__(self, prompt=[], config={}, session={}): - super().__init__() - self._prompt = prompt - self._config = config - self._wmt = "" - self._subshells = {} + def __init__(self, cache={}): + super().__init__(cache) + self._name = "DM Shell" + self._prompt = ['dm'] + self._toolbar = [('class:bold', ' DMSH ')] + self._key_bindings = BINDINGS self._register_subshells() self._register_keybindings() - self._session = session def _register_keybindings(self): - @bindings.add('c-q') - @bindings.add('c-d') + self._toolbar.extend([ + ('', " [H]elp "), + ('', " [W]ild Magic Table "), + ('', " [Q]uit "), + ]) + + @self.key_bindings.add('c-q') + @self.key_bindings.add('c-d') def quit(event): self.quit() - @bindings.add('c-h') + @self.key_bindings.add('c-h') def help(event): self.help() - @bindings.add('c-w') + @self.key_bindings.add('c-w') def wmt(event): self.wmt() - def _register_subshells(self): - for subclass in BasePrompt.__subclasses__(): - if subclass.__name__ == self.__class__.__name__: - continue - self._subshells[subclass.__name__] = subclass(parent=self) - - @property - def key_bindings(self): - return bindings - - @property - def toolbar(self): - return [ - ('class:bold', ' DMSH '), - ('', " [H]elp "), - ('', " [W]mt "), - ('', " [Q]uit "), - ] - - @property - def session(self): - return self._session - - @property - def autocomplete_values(self): - return list(self.commands.keys()) - - def default_completer(self, document, complete_event): # pragma: no cover - word = document.current_line_before_cursor - raise Exception(word) - - def process(self, cmd, *parts): - if cmd in self.commands: - return self.commands[cmd].handler(self, parts) - return "Unknown Command; try help." - @command(usage=""" [title]QUIT[/title] @@ -86,8 +55,10 @@ class InteractiveShell(BasePrompt): """ Quit dmsh. """ - get_app().exit() - raise SystemExit("Okay BYEEEE") + try: + get_app().exit() + finally: + raise SystemExit("") @command(usage=""" [title]HELP FOR THE HELP LORD[/title] @@ -99,7 +70,7 @@ class InteractiveShell(BasePrompt): [link]> help [COMMAND][/link] """) - def help(self, *parts): + def help(self, parts=[]): """ Display the help message. """ @@ -115,7 +86,7 @@ class InteractiveShell(BasePrompt): [link]id[/link] """) - def id(self, *parts): + def id(self, parts=[]): """ Increment the date by one day. """ @@ -136,13 +107,13 @@ class InteractiveShell(BasePrompt): "Gopher Gulch", "Calamity Ridge" ])) - def loc(self, *parts): + def loc(self, parts=[]): """ Move the party to a new region of the Sahwat Desert. """ if parts: - self.session['location'] = (' '.join(parts)) - self.console.print(f"The party is in {self.session['location']}.") + self.cache['location'] = (' '.join(parts)) + self.console.print(f"The party is in {self.cache['location']}.") @command(usage=""" [title]OVERLAND TRAVEL[/title] @@ -153,7 +124,7 @@ class InteractiveShell(BasePrompt): [link]ot in[/link] """) - def ot(self, *parts): + def ot(self, parts=[]): """ Increment the date by one day and record """ @@ -162,7 +133,7 @@ class InteractiveShell(BasePrompt): @command(usage=""" [title]WILD MAGIC TABLE[/title] - [b]wmt[/b] Generates a d20 wild magic surge roll table. The table will be cached for the session. + [b]wmt[/b] Generates a d20 wild magic surge roll table. The table will be cached for the cache. [title]USAGE[/title] @@ -178,14 +149,14 @@ class InteractiveShell(BasePrompt): """ Generate a Wild Magic Table for resolving spell effects. """ - if not self._wmt: + if 'wmt' not in self.cache: rt = RollTable( - [Path(f"{self._config['table_sources_path']}/{source}").read_text()], + [Path(f"{self.cache['table_sources_path']}/{source}").read_text()], frequency='default', die=20, ) table = Table(*rt.expanded_rows[0]) for row in rt.expanded_rows[1:]: table.add_row(*row) - self._wmt = table - self.console.print(self._wmt) + self.cache['wmt'] = table + self.console.print(self.cache['wmt'])