WTS/game/scripts/utility/console.rpy
LoafyLemon 555fe97833 Console
* Add line recall support
* Add skip adjacent repeating commands for recall
* Add execution timestamps
* Add separation for each consecutive script reload
* Fix history
2024-05-18 20:17:25 +01:00

614 lines
20 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):
self.command = command
self.result = result
self.is_error = is_error
self.timestamp = datetime.datetime.now().timestamp()
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 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)
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 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" action ToggleScreenVariable("is_modal", 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:
$ timestamp = datetime.datetime.fromtimestamp(entry.timestamp).strftime('%H:%M:%S')
$ timestamp = f"{{size=-2}}{{color=#ffffff80}}{timestamp}{{/color}}{{/size}}"
if entry.command is not None:
text "[timestamp!i] [entry.command!q]" style "console_command_text"
if entry.result is not None:
if entry.is_error:
text "[timestamp!i] {unicode}➥{/unicode} [entry.result!q]" style "console_error_text"
else:
text "[timestamp!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"
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:
adjust_spacing False