From cc9c68461b298ef0cb5d49b941f70d2ea6860607 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 15 Nov 2018 23:05:33 -0500 Subject: [PATCH 1/9] Initial shell. --- ppb/shell.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 ppb/shell.py diff --git a/ppb/shell.py b/ppb/shell.py new file mode 100644 index 00000000..3df3615e --- /dev/null +++ b/ppb/shell.py @@ -0,0 +1,48 @@ +import sys +import code +import ppb +import threading +import logging + + +class ReplThread(threading.Thread): + banner = """ + """ + def __init__(self, engine): + super().__init__(name='repl') + self.engine = engine + self.locals = self.build_locals() + + def build_locals(self): + return { + "__name__": "__console__", + "__doc__": None, + "Vector": ppb.Vector, + "BaseScene": ppb.BaseScene, + "BaseSprite": ppb.BaseSprite, + "current_scene": self.get_scene, + } + + def signal(self, event): + self.engine.signal(event) + + def get_scene(self): + return self.engine.current_scene + + def run(self): + # Stolen from stdlib code.interact() + console = code.InteractiveConsole(self.locals) + if sys.__interactivehook__ is not None: + sys.__interactivehook__() + + console.interact(banner=self.banner, exitmsg="") + self.signal(ppb.events.Quit()) + + +logging.basicConfig(level=logging.INFO) + +with ppb.GameEngine(ppb.BaseScene) as eng: + repl = ReplThread(eng) + repl.start() + eng.run() + print("Engine has quit") From f70aeaa91fa046eb67bce7bca91315523f69e61a Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 15 Nov 2018 23:08:45 -0500 Subject: [PATCH 2/9] Add a signal() to the repl's locals --- ppb/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ppb/shell.py b/ppb/shell.py index 3df3615e..de8b9c5c 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -21,6 +21,7 @@ def build_locals(self): "BaseScene": ppb.BaseScene, "BaseSprite": ppb.BaseSprite, "current_scene": self.get_scene, + "signal": self.signal, } def signal(self, event): From 954f1ec91c15c7f4e373948efad144c3582c98f5 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 15 Nov 2018 23:46:48 -0500 Subject: [PATCH 3/9] Add helpful BaseSprite, and less helpful Readline-compatible logging handler --- ppb/shell.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/ppb/shell.py b/ppb/shell.py index de8b9c5c..40483693 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -3,6 +3,30 @@ import ppb import threading import logging +import readline +import pathlib + + +class ReadlineHandler(logging.StreamHandler): + def emit(self, record): + # Blatently stolen from logging.StreamHandler.emit() + + # ANSI for "move to the begining of the line, then erase it" + self.stream.write("\r\x1B[2K") + + try: + msg = self.format(record) + self.stream.write(msg + self.terminator) + self.flush() + + except Exception: + self.handleError(record) + + readline.redisplay() + + +class BaseSprite(ppb.BaseSprite): + resource_path = pathlib.Path.cwd() class ReplThread(threading.Thread): @@ -19,7 +43,7 @@ def build_locals(self): "__doc__": None, "Vector": ppb.Vector, "BaseScene": ppb.BaseScene, - "BaseSprite": ppb.BaseSprite, + "BaseSprite": BaseSprite, "current_scene": self.get_scene, "signal": self.signal, } @@ -40,7 +64,7 @@ def run(self): self.signal(ppb.events.Quit()) -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.INFO, handlers=[ReadlineHandler()]) with ppb.GameEngine(ppb.BaseScene) as eng: repl = ReplThread(eng) From 8651b3f24d5ce639d316214dd36ad45a578a3825 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Fri, 16 Nov 2018 00:13:24 -0500 Subject: [PATCH 4/9] Handle logging well with prompts --- ppb/shell.py | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/ppb/shell.py b/ppb/shell.py index 40483693..922db0c3 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -7,23 +7,38 @@ import pathlib -class ReadlineHandler(logging.StreamHandler): +class InteractiveConsole(code.InteractiveConsole): + last_prompt = "" + + def raw_input(self, prompt): + self.last_prompt = prompt + return super().raw_input(prompt) + + def interject(self, text): + sys.stdout.write( + "\r\x1B[2K" # ANSI for "move to the begining of the line, then erase it" + + text + + "\n" + + self.last_prompt + # readline.redisplay() doesn't seem to do anything + + readline.get_line_buffer() + ) + + +class ReadlineHandler(logging.Handler): + def __init__(self, *p, **kw): + super().__init__(*p, **kw) + self.output = print + def emit(self, record): # Blatently stolen from logging.StreamHandler.emit() - - # ANSI for "move to the begining of the line, then erase it" - self.stream.write("\r\x1B[2K") - try: msg = self.format(record) - self.stream.write(msg + self.terminator) - self.flush() + self.output(msg) except Exception: self.handleError(record) - readline.redisplay() - class BaseSprite(ppb.BaseSprite): resource_path = pathlib.Path.cwd() @@ -56,18 +71,26 @@ def get_scene(self): def run(self): # Stolen from stdlib code.interact() - console = code.InteractiveConsole(self.locals) + self.console = InteractiveConsole(self.locals) if sys.__interactivehook__ is not None: sys.__interactivehook__() - console.interact(banner=self.banner, exitmsg="") + self.console.interact(banner=self.banner, exitmsg="") self.signal(ppb.events.Quit()) + def interject(self, text): + if self.console is None: + print(text) + else: + self.console.interject(text) + -logging.basicConfig(level=logging.INFO, handlers=[ReadlineHandler()]) +handler = ReadlineHandler() +logging.basicConfig(level=logging.INFO, handlers=[handler]) with ppb.GameEngine(ppb.BaseScene) as eng: repl = ReplThread(eng) + handler.output = repl.interject repl.start() eng.run() print("Engine has quit") From 409b5689fac85d72ea8e7e8b48dd3b139a40a30b Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Fri, 16 Nov 2018 00:29:27 -0500 Subject: [PATCH 5/9] Add banner and expand the default locals --- ppb/shell.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ppb/shell.py b/ppb/shell.py index 922db0c3..c7565b8e 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -5,6 +5,7 @@ import logging import readline import pathlib +import textwrap class InteractiveConsole(code.InteractiveConsole): @@ -23,7 +24,7 @@ def interject(self, text): # readline.redisplay() doesn't seem to do anything + readline.get_line_buffer() ) - + class ReadlineHandler(logging.Handler): def __init__(self, *p, **kw): @@ -46,6 +47,14 @@ class BaseSprite(ppb.BaseSprite): class ReplThread(threading.Thread): banner = """ + PPB Interactive Console + + Vector, BaseSprite, BaseScene, ppb and several other things are imported. + + current_scene() gets the current scene. + signal() injects an event. + + Type "help" for more information. """ def __init__(self, engine): super().__init__(name='repl') @@ -58,7 +67,12 @@ def build_locals(self): "__doc__": None, "Vector": ppb.Vector, "BaseScene": ppb.BaseScene, - "BaseSprite": BaseSprite, + "BaseSprite": BaseSprite, + "DoNotRender": ppb.flags.DoNotRender, + "ppb": ppb, + "events": ppb.events, + "keycodes": ppb.keycodes, + "buttons": ppb.buttons, "current_scene": self.get_scene, "signal": self.signal, } @@ -75,7 +89,7 @@ def run(self): if sys.__interactivehook__ is not None: sys.__interactivehook__() - self.console.interact(banner=self.banner, exitmsg="") + self.console.interact(banner=textwrap.dedent(self.banner), exitmsg="") self.signal(ppb.events.Quit()) def interject(self, text): From ca6bf5737b691782e2d957fba7790e008aa742b6 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Fri, 16 Nov 2018 00:30:46 -0500 Subject: [PATCH 6/9] Interject the "Engine has quit" message --- ppb/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ppb/shell.py b/ppb/shell.py index c7565b8e..7cb612ac 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -107,4 +107,4 @@ def interject(self, text): handler.output = repl.interject repl.start() eng.run() - print("Engine has quit") + repl.interject("Engine has quit") From 0530ab4dc4270856f71bae2cf948531dee077386 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 22 Nov 2018 00:30:15 -0500 Subject: [PATCH 7/9] Have the starting scene pull from repl context --- ppb/shell.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/ppb/shell.py b/ppb/shell.py index 7cb612ac..e4828873 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -6,6 +6,7 @@ import readline import pathlib import textwrap +import atexit class InteractiveConsole(code.InteractiveConsole): @@ -24,7 +25,10 @@ def interject(self, text): # readline.redisplay() doesn't seem to do anything + readline.get_line_buffer() ) + sys.stdout.flush() +# Reset terminal on exit +# atexit.register(print, "\x1Bc", flush=True) class ReadlineHandler(logging.Handler): def __init__(self, *p, **kw): @@ -51,6 +55,9 @@ class ReplThread(threading.Thread): Vector, BaseSprite, BaseScene, ppb and several other things are imported. + scene is preloaded with the starting scene. Defining things in the REPL show + up on the scene. + current_scene() gets the current scene. signal() injects an event. @@ -89,8 +96,10 @@ def run(self): if sys.__interactivehook__ is not None: sys.__interactivehook__() - self.console.interact(banner=textwrap.dedent(self.banner), exitmsg="") - self.signal(ppb.events.Quit()) + try: + self.console.interact(banner=textwrap.dedent(self.banner), exitmsg="") + finally: + self.signal(ppb.events.Quit()) def interject(self, text): if self.console is None: @@ -98,11 +107,27 @@ def interject(self, text): else: self.console.interject(text) +repl = None + +class ReplScene(ppb.BaseScene): + def __init__(self, *p, **kw): + super().__init__(*p, **kw) + repl.locals['scene'] = self + + def __getattribute__(self, name): + global repl + if repl is not None: + try: + return repl.locals[name] + except KeyError: + pass # Fall through + return super().__getattribute__(name) + handler = ReadlineHandler() logging.basicConfig(level=logging.INFO, handlers=[handler]) -with ppb.GameEngine(ppb.BaseScene) as eng: +with ppb.GameEngine(ReplScene) as eng: repl = ReplThread(eng) handler.output = repl.interject repl.start() From 7591f20cd315090e95802a9836424ab38d3143c5 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 22 Nov 2018 00:43:00 -0500 Subject: [PATCH 8/9] Clarify that bit. --- ppb/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ppb/shell.py b/ppb/shell.py index e4828873..6e6d9dc5 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -56,7 +56,7 @@ class ReplThread(threading.Thread): Vector, BaseSprite, BaseScene, ppb and several other things are imported. scene is preloaded with the starting scene. Defining things in the REPL show - up on the scene. + up on the scene. Event handlers work. current_scene() gets the current scene. signal() injects an event. From 76cc91b817130ef682489457470372c76fecdd47 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 24 Nov 2018 02:11:46 -0500 Subject: [PATCH 9/9] Handle exceptions and Ctrl-C better --- ppb/shell.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ppb/shell.py b/ppb/shell.py index 6e6d9dc5..3280d5a4 100644 --- a/ppb/shell.py +++ b/ppb/shell.py @@ -7,6 +7,7 @@ import pathlib import textwrap import atexit +import signal class InteractiveConsole(code.InteractiveConsole): @@ -114,6 +115,17 @@ def __init__(self, *p, **kw): super().__init__(*p, **kw) repl.locals['scene'] = self + last_event_error = None + + def __event__(self, event, signal): + try: + super().__event__(event, signal) + except Exception: + event_name = type(event).__name__ + if self.last_event_error != event_name: + logging.exception("Error handling %s event", event_name) + self.last_event_error = event_name + def __getattribute__(self, name): global repl if repl is not None: @@ -127,6 +139,10 @@ def __getattribute__(self, name): handler = ReadlineHandler() logging.basicConfig(level=logging.INFO, handlers=[handler]) +# Don't let Ctrl-C kill the game engine +# FIXME: Figure out some way to raise KeyboardInterrupt in the repl thread +signal.signal(signal.SIGINT, signal.SIG_IGN) + with ppb.GameEngine(ReplScene) as eng: repl = ReplThread(eng) handler.output = repl.interject