WTS/game/scripts/utility/console.rpy

739 lines
25 KiB
Plaintext

default persistent.console_history = None
default persistent.console_line_history = None
init 1702 python in _console:
import datetime
class ScriptErrorHandlerConsole(object):
"""
Handles error in Ren'Py script.
"""
def __init__(self):
self.target_depth = renpy.call_stack_depth()
def __call__(self, short, full, traceback_fn):
he = console.history[-1]
he.result = short.split("\n")[-2]
he.is_error = True
if renpy.get_return_stack():
while renpy.call_stack_depth() > self.target_depth:
renpy.pop_call()
renpy.jump("console")
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():
# Can we run Ren'Py code?
name = renpy.load_string(code + "\nreturn")
if name is not None:
renpy.game.context().exception_handler = ScriptErrorHandlerConsole()
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")
renpy.return_statement()
@command()
def quit(l):
renpy.hide_screen("console")
renpy.return_statement()
@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_entries = find_matching_entries(labels, partial)
if not matching_entries:
return "No matching labels found."
# Sort the matching entries alphabetically
matching_entries.sort()
# Calculate the number of columns for the grid
num_columns = 3
num_rows = (len(matching_entries) + num_columns - 1) // num_columns
# Fill the grid by columns to preserve alphabetical order
grid = [[] for _ in range(num_rows)]
for i, entry in enumerate(matching_entries):
grid[i % num_rows].append(entry)
# Find the maximum width of each column
column_widths = [max(len(grid[row][col]) if col < len(grid[row]) else 0 for row in range(num_rows)) for col in range(num_columns)]
# Format the rows into a grid layout
formatted_rows = []
for row in grid:
formatted_row = []
for i, item in enumerate(row):
formatted_row.append(item.ljust(column_widths[i]))
formatted_rows.append(" | ".join(formatted_row))
matching = "\n".join(formatted_rows)
return "\n" + 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)
label console:
show screen console
$ renpy.game.context().force_checkpoint = True
$ renpy.exports.checkpoint(hard="not_greedy")
$ ui.interact()
return
init python in _console:
import pygame
import re
def enter():
if console is None:
return
console.start()
if renpy.game.context().rollback:
try:
renpy.rollback(checkpoints=0, force=True, greedy=False, current_label="console")
except renpy.game.CONTROL_EXCEPTIONS:
raise
except Exception:
pass
renpy.call_in_new_context("console")
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])
console.reset()
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
spacing 10
if is_modal:
textbutton "Modal: [is_modal]" action [ToggleScreenVariable("is_modal", True, False), Return()]
else:
textbutton "Modal: [is_modal]" action [ToggleScreenVariable("is_modal", True, False), Hide("console"), _console.enter]
textbutton "Include STDOUT: [include_stdout]" action ToggleScreenVariable("include_stdout", True, False)
text "Ren'py Scripting: [scripting]"
viewport:
style_prefix "_console"
mousewheel is_modal
scrollbars "vertical"
yinitial 1.0
ymaximum 1040
id "consolevp"
if not is_modal:
at transform:
alpha 0.5
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 is_modal:
if entry.command is not None:
textbutton "[info!i][entry.command!q]" style "console_command" action [CopyToClipboard(entry.command), Notify("Copied to clipboard.")]
if entry.result is not None:
if entry.is_error:
textbutton "[info!i]{unicode}➥{/unicode} [entry.result!q]" style "console_error" action [CopyToClipboard(entry.result), Notify("Copied to clipboard.")]
else:
textbutton "[info!i]{unicode}➥{/unicode} [entry.result!q]" style "console_result" action [CopyToClipboard(entry.result), Notify("Copied to clipboard.")]
else:
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
if is_modal:
key "console_exit" action [Hide("console"), Return()]
else:
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 is empty:
hover_background "#ffffff25"
style console_line_button is empty:
xfill True
hover_background "#ffffff25"
style console_command is console_line_button
style console_error is console_line_button
style console_result is console_line_button
style console_button_text is console_text:
adjust_spacing True
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