LoafyLemon
5d80a963ae
* Add STDIO and STDERR support. * Add toggle to hide STDOUT entries altogether. * Improved styling.
638 lines
21 KiB
Plaintext
638 lines
21 KiB
Plaintext
default persistent.console_history = None
|
|
default persistent.console_line_history = None
|
|
|
|
init 1702 python in _console:
|
|
import datetime
|
|
|
|
class ConsoleHistoryEntry(object):
|
|
lines = 0
|
|
|
|
def __init__(self, command, result=None, is_error=False, is_stdio=False):
|
|
self.command = command
|
|
self.result = result
|
|
self.is_error = is_error
|
|
self.timestamp = datetime.datetime.now().timestamp()
|
|
self.is_stdio = is_stdio
|
|
|
|
def update_lines(self):
|
|
|
|
if self.result is None:
|
|
return
|
|
|
|
lines = self.result
|
|
if len(lines) > config.console_history_lines * 160:
|
|
lines = "…" + self.result[-config.console_history_lines * 160:]
|
|
|
|
lines = lines.split("\n")
|
|
|
|
if len(lines) > config.console_history_lines:
|
|
lines = [ "…" ] + lines[-config.console_history_lines:]
|
|
|
|
self.result = "\n".join(lines)
|
|
self.lines = len(lines)
|
|
|
|
class DebugConsoleNew(object):
|
|
|
|
def __init__(self):
|
|
|
|
self.history = BoundedList(config.console_history_size, config.console_history_lines + config.console_history_size)
|
|
self.line_history = BoundedList(config.console_history_size)
|
|
self.line_index = 0
|
|
|
|
if persistent.console_history is not None:
|
|
for i in persistent.console_history:
|
|
he = ConsoleHistoryEntry(i[0], i[1], i[2])
|
|
he.update_lines()
|
|
self.history.append(he)
|
|
|
|
if persistent.console_line_history is not None:
|
|
self.line_history.extend(persistent.console_line_history)
|
|
|
|
self.first_time = True
|
|
self.renpy_scripting = False
|
|
|
|
self.reset()
|
|
|
|
def backup(self):
|
|
persistent.console_history = [ (i.command, i.result, i.is_error) for i in self.history ]
|
|
persistent.console_line_history = list(self.line_history)
|
|
|
|
def start(self):
|
|
he = ConsoleHistoryEntry(None)
|
|
|
|
if self.first_time:
|
|
current_time = datetime.datetime.now()
|
|
|
|
message = ("-" * 50) + " RESTART OCCURED " + ("-" * 50)
|
|
he.result = message
|
|
he.update_lines()
|
|
self.history.append(he)
|
|
self.first_time = False
|
|
|
|
if self.can_renpy():
|
|
self.renpy_scripting = True
|
|
else:
|
|
self.renpy_scripting = False
|
|
|
|
def reset(self):
|
|
self.lines = [ "" ]
|
|
self.line_index = len(self.line_history)
|
|
|
|
def recall_line(self, offset):
|
|
|
|
self.line_index += offset
|
|
|
|
if self.line_index < 0:
|
|
self.line_index = 0
|
|
|
|
if self.line_index > len(self.line_history):
|
|
self.line_index = len(self.line_history)
|
|
|
|
# Skip adjacent duplicates
|
|
while self.line_index > 0 and self.line_index < len(self.line_history) and self.line_history[self.line_index] == self.line_history[self.line_index - 1]:
|
|
self.line_index += offset
|
|
|
|
if self.line_index == len(self.line_history):
|
|
self.lines = [ "" ]
|
|
else:
|
|
self.lines = list(self.line_history[self.line_index])
|
|
|
|
cs = renpy.get_screen("console")
|
|
|
|
scope = cs.scope
|
|
consoleinput = scope["consoleinput"]
|
|
consoleinput.caret_pos = 0
|
|
consoleinput.update_text(self.lines[-1], consoleinput.editable)
|
|
renpy.restart_interaction()
|
|
|
|
def older(self):
|
|
self.recall_line(-1)
|
|
|
|
def newer(self):
|
|
self.recall_line(1)
|
|
|
|
def show_stdio(self):
|
|
|
|
old_entry = None
|
|
|
|
if persistent._console_short:
|
|
if len(stdio_lines) > 30:
|
|
stdio_lines[:] = stdio_lines[:10] + [ (False, " ... ") ] + stdio_lines[-20:]
|
|
|
|
for error, l in stdio_lines:
|
|
if persistent._console_short:
|
|
if len(l) > 200:
|
|
l = l[:100] + "..." + l[-100:]
|
|
|
|
if (old_entry is not None) and (error == old_entry.is_error):
|
|
old_entry.result += "\n" + l
|
|
else:
|
|
e = ConsoleHistoryEntry(None, l, error)
|
|
e.update_lines()
|
|
self.history.append(e)
|
|
old_entry = e
|
|
|
|
if old_entry is not None:
|
|
old_entry.update_lines()
|
|
|
|
stdio_lines[:] = _list()
|
|
|
|
def stdout_line(self, l):
|
|
if not (config.console or config.developer):
|
|
return
|
|
|
|
he = ConsoleHistoryEntry(l, is_stdio=True)
|
|
he.update_lines()
|
|
self.history.append(he)
|
|
|
|
def stderr_line(self, l):
|
|
if not (config.console or config.developer):
|
|
return
|
|
|
|
he = ConsoleHistoryEntry(l, is_error=True, is_stdio=True)
|
|
he.update_lines()
|
|
|
|
def can_renpy(self):
|
|
"""
|
|
Returns true if we can run Ren'Py code.
|
|
"""
|
|
|
|
return renpy.game.context().rollback
|
|
|
|
def format_exception(self):
|
|
etype, evalue, etb = sys.exc_info()
|
|
return traceback.format_exception_only(etype, evalue)[-1]
|
|
|
|
def run(self, lines):
|
|
|
|
line_count = len(lines)
|
|
code = "\n".join(lines)
|
|
|
|
he = ConsoleHistoryEntry(code)
|
|
self.history.append(he)
|
|
self.line_history.append(lines)
|
|
|
|
try:
|
|
|
|
# If we have 1 line, try to parse it as a command.
|
|
if line_count == 1:
|
|
block = [ ( "<console>", 1, code, [ ]) ]
|
|
l = renpy.parser.Lexer(block)
|
|
l.advance()
|
|
|
|
# Command can be None, but that's okay, since the lookup will fail.
|
|
command = l.word()
|
|
|
|
command_fn = config.console_commands.get(command, None)
|
|
|
|
if command_fn is not None:
|
|
he.result = command_fn(l)
|
|
he.update_lines()
|
|
return
|
|
|
|
error = None
|
|
|
|
# Try to run it as Ren'Py.
|
|
if self.can_renpy():
|
|
|
|
# TODO: Can we run Ren'Py code?
|
|
name = renpy.load_string(code + "\nreturn")
|
|
|
|
if name is not None:
|
|
renpy.game.context().exception_handler = ScriptErrorHandler()
|
|
renpy.call(name)
|
|
else:
|
|
error = "\n\n".join(renpy.get_parse_errors())
|
|
|
|
# Try to eval it.
|
|
try:
|
|
renpy.python.py_compile(code, 'eval')
|
|
except Exception:
|
|
pass
|
|
else:
|
|
result = renpy.python.py_eval(code)
|
|
if persistent._console_short and not getattr(result, "_console_always_long", False):
|
|
he.result = aRepr.repr(result)
|
|
else:
|
|
he.result = repr(result)
|
|
|
|
he.update_lines()
|
|
return
|
|
|
|
# Try to exec it.
|
|
try:
|
|
renpy.python.py_compile(code, "exec")
|
|
except Exception:
|
|
if error is None:
|
|
error = self.format_exception()
|
|
else:
|
|
renpy.python.py_exec(code)
|
|
return
|
|
|
|
if error is not None:
|
|
he.result = error
|
|
he.update_lines()
|
|
he.is_error = True
|
|
|
|
except renpy.game.CONTROL_EXCEPTIONS:
|
|
raise
|
|
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
he.result = self.format_exception().rstrip()
|
|
he.update_lines()
|
|
he.is_error = True
|
|
|
|
console = DebugConsoleNew()
|
|
|
|
@command(_("exit: exit the console"))
|
|
def exit(l):
|
|
renpy.hide_screen("console")
|
|
|
|
@command()
|
|
def quit(l):
|
|
renpy.hide_screen("console")
|
|
|
|
@command()
|
|
def stack(l):
|
|
console.run(["renpy.exports.get_return_stack()"])
|
|
|
|
@command(_("call <label>: calls label"))
|
|
def call(l):
|
|
label = l.label_name()
|
|
|
|
if label is None:
|
|
raise Exception("Could not parse label. (Unqualified local labels are not allowed.)")
|
|
|
|
if not console.can_renpy():
|
|
raise Exception("Ren'Py script not enabled. Not calling.")
|
|
|
|
if not renpy.has_label(label):
|
|
raise Exception("Label %s not found." % label)
|
|
|
|
# renpy.pop_call()
|
|
renpy.call(label)
|
|
|
|
@command(_("jump <label>: jumps to label"))
|
|
def jump(l):
|
|
label = l.label_name()
|
|
|
|
if label is None:
|
|
raise Exception("Could not parse label. (Unqualified local labels are not allowed.)")
|
|
|
|
if not console.can_renpy():
|
|
raise Exception("Ren'Py script not enabled. Not jumping.")
|
|
|
|
if not renpy.has_label(label):
|
|
raise Exception("Label %s not found." % label)
|
|
|
|
# renpy.pop_call()
|
|
renpy.jump(label)
|
|
|
|
@command(_("find <label partial>: returns matching labels"))
|
|
def find(l):
|
|
def find_matching_entries(labels, partial):
|
|
return [entry for entry in labels if partial in entry]
|
|
|
|
partial = l.rest()
|
|
labels = renpy.get_all_labels()
|
|
|
|
matching = find_matching_entries(labels, partial)
|
|
raise Exception("Matching labels:\n %s" % matching)
|
|
|
|
renpy.config.at_exit_callbacks.append(console.backup)
|
|
renpy.config.stdout_callbacks.append(console.stdout_line)
|
|
renpy.config.stderr_callbacks.append(console.stderr_line)
|
|
|
|
init python in _console:
|
|
import pygame
|
|
import re
|
|
|
|
def enter():
|
|
if console is None:
|
|
return
|
|
|
|
console.start()
|
|
|
|
renpy.show_screen("console")
|
|
renpy.restart_interaction()
|
|
|
|
map_event = renpy.display.behavior.map_event
|
|
|
|
class ConsoleInput(renpy.display.behavior.Input):
|
|
def event(self, ev, x, y, st):
|
|
|
|
self.st = st
|
|
self.old_caret_pos = self.caret_pos
|
|
|
|
if not self.editable:
|
|
return None
|
|
|
|
edit_controls = any([
|
|
map_event(ev, "input_jump_word_left"),
|
|
map_event(ev, "input_jump_word_right"),
|
|
map_event(ev, "input_delete_word"),
|
|
map_event(ev, "input_delete_full"),
|
|
])
|
|
|
|
if (ev.type == pygame.KEYDOWN) and (pygame.key.get_mods() & pygame.KMOD_LALT) and (not ev.unicode) and not edit_controls:
|
|
return None
|
|
|
|
l = len(self.content)
|
|
|
|
raw_text = None
|
|
|
|
if map_event(ev, "input_backspace"):
|
|
|
|
if self.content and self.caret_pos > 0:
|
|
content = self.content[0:self.caret_pos - 1] + self.content[self.caret_pos:l]
|
|
self.caret_pos -= 1
|
|
self.update_text(content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif self.multiline and map_event(ev, 'input_next_line'):
|
|
content = self.content[:self.caret_pos] + '\n' + self.content[self.caret_pos:]
|
|
self.caret_pos += 1
|
|
self.update_text(content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_enter"):
|
|
|
|
content = self.content.strip()
|
|
|
|
if not content:
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
if self.edit_text:
|
|
content = content[0:self.caret_pos] + self.edit_text + self.content[self.caret_pos:]
|
|
|
|
if self.value:
|
|
self.value.enter()
|
|
|
|
if not self.changed:
|
|
self.caret_pos = 0
|
|
self.update_text(" ", self.editable)
|
|
widget = renpy.get_widget("console", "consolevp")
|
|
widget.yadjustment._value = 9999999
|
|
console.run([content])
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
renpy.restart_interaction()
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_left"):
|
|
if self.caret_pos > 0:
|
|
self.caret_pos -= 1
|
|
self.update_text(self.content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_jump_word_left"):
|
|
if self.caret_pos > 0:
|
|
space_pos = 0
|
|
for item in re.finditer(r"\s+", self.content[:self.caret_pos]):
|
|
_start, end = item.span()
|
|
if end != self.caret_pos:
|
|
space_pos = end
|
|
self.caret_pos = space_pos
|
|
self.update_text(self.content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_right"):
|
|
if self.caret_pos < l:
|
|
self.caret_pos += 1
|
|
self.update_text(self.content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_jump_word_right"):
|
|
if self.caret_pos < l:
|
|
space_pos = l
|
|
for item in re.finditer(r"\s+", self.content[self.caret_pos + 1:]):
|
|
start, end = item.span()
|
|
space_pos = end
|
|
break
|
|
self.caret_pos = min(space_pos + self.caret_pos + 1, l)
|
|
self.update_text(self.content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_delete"):
|
|
if self.caret_pos < l:
|
|
content = self.content[0:self.caret_pos] + self.content[self.caret_pos + 1:l]
|
|
self.update_text(content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_delete_word"):
|
|
if self.caret_pos <= l:
|
|
space_pos = 0
|
|
for item in re.finditer(r"\s+", self.content[:self.caret_pos]):
|
|
start, end = item.span()
|
|
if end != self.caret_pos:
|
|
space_pos = end
|
|
content = self.content[0:space_pos] + self.content[self.caret_pos:l]
|
|
self.caret_pos = space_pos
|
|
self.update_text(content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_delete_full"):
|
|
if self.caret_pos <= l:
|
|
content = self.content[self.caret_pos:l]
|
|
self.caret_pos = 0
|
|
self.update_text(content, self.editable)
|
|
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_home"):
|
|
self.caret_pos = 0
|
|
self.update_text(self.content, self.editable)
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif map_event(ev, "input_end"):
|
|
self.caret_pos = l
|
|
self.update_text(self.content, self.editable)
|
|
renpy.display.render.redraw(self, 0)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif self.copypaste and map_event(ev, "input_copy"):
|
|
text = self.content.encode("utf-8")
|
|
pygame.scrap.put(pygame.scrap.SCRAP_TEXT, text)
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif self.copypaste and map_event(ev, "input_paste"):
|
|
text = pygame.scrap.get(pygame.scrap.SCRAP_TEXT)
|
|
text = text.decode("utf-8")
|
|
raw_text = ""
|
|
for c in text:
|
|
if ord(c) >= 32:
|
|
raw_text += c
|
|
|
|
elif ev.type == pygame.TEXTEDITING:
|
|
self.update_text(self.content, self.editable, check_size=True)
|
|
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
elif ev.type == pygame.TEXTINPUT:
|
|
self.edit_text = ""
|
|
raw_text = ev.text
|
|
|
|
elif ev.type == pygame.KEYDOWN:
|
|
|
|
if ev.unicode and ord(ev.unicode[0]) >= 32:
|
|
raw_text = ev.unicode
|
|
elif renpy.display.interface.text_event_in_queue():
|
|
raise renpy.display.core.IgnoreEvent()
|
|
elif (32 <= ev.key < 127) and not (ev.mod & (pygame.KMOD_ALT | pygame.KMOD_META)):
|
|
# Ignore printable keycodes without unicode.
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
if raw_text is not None:
|
|
|
|
text = ""
|
|
|
|
for c in raw_text:
|
|
|
|
# Allow is given
|
|
if self.allow:
|
|
|
|
# Allow is regex
|
|
if isinstance(self.allow, re.Pattern):
|
|
|
|
# Character doesn't match
|
|
if self.allow.search(c) is None:
|
|
continue
|
|
|
|
# Allow is string
|
|
elif c not in self.allow:
|
|
continue
|
|
|
|
# Exclude is given
|
|
if self.exclude:
|
|
|
|
# Exclude is regex
|
|
if isinstance(self.exclude, re.Pattern):
|
|
|
|
# Character matches
|
|
if self.exclude.search(c) is not None:
|
|
continue
|
|
|
|
# Exclude is string
|
|
elif c in self.exclude:
|
|
continue
|
|
|
|
text += c
|
|
|
|
if self.length:
|
|
remaining = self.length - len(self.content)
|
|
text = text[:remaining]
|
|
|
|
if text:
|
|
|
|
content = self.content[0:self.caret_pos] + text + self.content[self.caret_pos:l]
|
|
self.caret_pos += len(text)
|
|
|
|
self.update_text(content, self.editable, check_size=True)
|
|
|
|
raise renpy.display.core.IgnoreEvent()
|
|
|
|
screen console:
|
|
layer "interface"
|
|
zorder 999
|
|
style_prefix "console"
|
|
|
|
$ history = _console.console.history
|
|
$ scripting = _console.console.renpy_scripting
|
|
default is_modal = True
|
|
default include_stdout = True
|
|
default consoleinput = _console.ConsoleInput("", style="console_input_text", exclude="", replaces=True, copypaste=True)
|
|
|
|
if is_modal:
|
|
button action NullAction() style "empty"
|
|
|
|
frame:
|
|
yfill True
|
|
left_padding 10
|
|
|
|
has vbox
|
|
|
|
hbox:
|
|
yfill False
|
|
textbutton "Modal: [is_modal]" action ToggleScreenVariable("is_modal", True, False)
|
|
textbutton "Include STDOUT: [include_stdout]" action ToggleScreenVariable("include_stdout", True, False)
|
|
text "Ren'py Scripting: [scripting]"
|
|
|
|
viewport:
|
|
style_prefix "_console"
|
|
mousewheel True
|
|
scrollbars "vertical"
|
|
yinitial 1.0
|
|
ymaximum 1040
|
|
id "consolevp"
|
|
|
|
has vbox
|
|
|
|
for entry in history:
|
|
|
|
if not include_stdout and entry.is_stdio:
|
|
continue
|
|
|
|
$ timestamp = datetime.datetime.fromtimestamp(entry.timestamp).strftime('%H:%M:%S')
|
|
$ stdout = " $ " if entry.is_stdio else " "
|
|
$ info = f"{{size=-2}}{{color=#ffffff80}}{timestamp}{{/color}}{{color=#228b22}}{stdout}{{/color}}{{/size}}"
|
|
|
|
if entry.command is not None:
|
|
text "[info!i][entry.command!q]" style "console_command_text"
|
|
|
|
if entry.result is not None:
|
|
if entry.is_error:
|
|
text "[info!i]{unicode}➥{/unicode} [entry.result!q]" style "console_error_text"
|
|
else:
|
|
text "[info!i]{unicode}➥{/unicode} [entry.result!q]" style "console_result_text"
|
|
|
|
hbox:
|
|
text "Command:"
|
|
add consoleinput
|
|
|
|
key "console_exit" action Hide("console")
|
|
key "console_older" action _console.console.older
|
|
key "console_newer" action _console.console.newer
|
|
|
|
style console_frame:
|
|
background "#20202080"
|
|
|
|
style console_text:
|
|
color "#ffffff"
|
|
outlines [(1, "#000000", 1, 0)]
|
|
size 14
|
|
font "gui/creamy_pumpkin_pie/fonts/Hack-Regular.ttf"
|
|
adjust_spacing False
|
|
|
|
style console_button_text is console_text
|
|
|
|
style console_command_text is console_text
|
|
|
|
style console_error_text is console_text:
|
|
color "#ff8080"
|
|
|
|
style console_result_text is console_text:
|
|
color "#ffffff"
|
|
|
|
style console_input_text is console_text
|