class Editor(object): """ The main class. Containing the whole editor. """ def __init__(self, config_directory='~/.pyvim'): # Vi options. self.show_line_numbers = True self.highlight_search = True self.paste_mode = False self.show_ruler = True self.show_wildmenu = True self.expand_tab = True # Insect spaces instead of tab characters. self.tabstop = 4 # Number of spaces that a tab character represents. self.incsearch = True # Show matches while typing search string. self.ignore_case = False # Ignore case while searching. self.enable_mouse_support = True self.display_unprintable_characters = True # ':set list' self.enable_jedi = True # ':set jedi', for Python Jedi completion. self.scroll_offset = 0 # ':set scrolloff' self.relative_number = False # ':set relativenumber' self.wrap_lines = True # ':set wrap' # Ensure config directory exists. self.config_directory = os.path.abspath(os.path.expanduser(config_directory)) if not os.path.exists(self.config_directory): os.mkdir(self.config_directory) self._reporters_running_for_buffer_names = set() self.window_arrangement = WindowArrangement(self) self.message = None # Load styles. (Mapping from name to Style class.) self.styles = generate_built_in_styles() self.current_style = get_editor_style_by_name('default') # I/O backends. self.io_backends = [ DirectoryIO(), HttpIO(), GZipFileIO(), # Should come before FileIO. FileIO(), ] # Create eventloop. self.eventloop = create_eventloop() # Create key bindings manager self.key_bindings_manager = create_key_bindings(self) # Create layout and CommandLineInterface instance. self.editor_layout = EditorLayout( self, self.key_bindings_manager, self.window_arrangement) self.application = self._create_application() self.cli = CommandLineInterface( eventloop=self.eventloop, application=self.application) # Start in navigation mode. self.key_bindings_manager.get_vi_state(self.cli).input_mode = InputMode.NAVIGATION # Hide message when a key is pressed. def key_pressed(): self.message = None self.cli.input_processor.beforeKeyPress += key_pressed # Command line previewer. self.previewer = CommandPreviewer(self) def load_initial_files(self, locations, in_tab_pages=False, hsplit=False, vsplit=False): """ Load a list of files. """ assert in_tab_pages + hsplit + vsplit <= 1 # Max one of these options. # When no files were given, open at least one empty buffer. locations2 = locations or [None] # First file self.window_arrangement.open_buffer(locations2[0]) for f in locations2[1:]: if in_tab_pages: self.window_arrangement.create_tab(f) elif hsplit: self.window_arrangement.hsplit(location=f) elif vsplit: self.window_arrangement.vsplit(location=f) else: self.window_arrangement.open_buffer(f) self.window_arrangement.active_tab_index = 0 if locations and len(locations) > 1: self.show_message('%i files loaded.' % len(locations)) def _create_application(self): """ Create CommandLineInterface instance. """ # Create Vi command buffer. def handle_action(cli, buffer): ' When enter is pressed in the Vi command line. ' text = buffer.text # Remember: leave_command_mode resets the buffer. # First leave command mode. We want to make sure that the working # pane is focussed again before executing the command handlers. self.leave_command_mode(append_to_history=True) # Execute command. handle_command(self, text) # Create history and search buffers. commands_history = FileHistory(os.path.join(self.config_directory, 'commands_history')) command_buffer = Buffer(accept_action=AcceptAction(handler=handle_action), enable_history_search=Always(), completer=create_command_completer(self), history=commands_history) search_buffer_history = FileHistory(os.path.join(self.config_directory, 'search_history')) search_buffer = Buffer(history=search_buffer_history, enable_history_search=Always(), accept_action=AcceptAction.IGNORE) # Create app. # Create CLI. application = Application( layout=self.editor_layout.layout, key_bindings_registry=self.key_bindings_manager.registry, get_title=lambda: get_terminal_title(self), buffers={ COMMAND_BUFFER: command_buffer, SEARCH_BUFFER: search_buffer, }, style=DynamicStyle(lambda: self.current_style), paste_mode=Condition(lambda cli: self.paste_mode), ignore_case=Condition(lambda cli: self.ignore_case), mouse_support=Condition(lambda cli: self.enable_mouse_support), use_alternate_screen=True, on_buffer_changed=Callback(self._current_buffer_changed)) # Handle command line previews. # (e.g. when typing ':colorscheme blue', it should already show the # preview before pressing enter.) def preview(): if self.cli.current_buffer == command_buffer: self.previewer.preview(command_buffer.text) command_buffer.on_text_changed += preview return application @property def current_editor_buffer(self): """ Return the `EditorBuffer` that is currently active. """ # For each buffer name on the focus stack. for current_buffer_name in self.cli.buffers.focus_stack: if current_buffer_name is not None: # Find/return the EditorBuffer with this name. for b in self.window_arrangement.editor_buffers: if b.buffer_name == current_buffer_name: return b @property def add_key_binding(self): """ Shortcut for adding new key bindings. (Mostly useful for a pyvimrc file, that receives this Editor instance as input.) """ return self.key_bindings_manager.registry.add_binding def show_message(self, message): """ Set a warning message. The layout will render it as a "pop-up" at the bottom. """ self.message = message def use_colorscheme(self, name='default'): """ Apply new colorscheme. (By name.) """ try: self.current_style = get_editor_style_by_name(name) except pygments.util.ClassNotFound: pass def sync_with_prompt_toolkit(self): """ Update the prompt-toolkit Layout and FocusStack. """ # After executing a command, make sure that the layout of # prompt-toolkit matches our WindowArrangement. self.editor_layout.update() # Make sure that the focus stack of prompt-toolkit has the current # page. self.cli.focus( self.window_arrangement.active_editor_buffer.buffer_name) def _current_buffer_changed(self, cli): """ Current buffer changed. """ name = self.cli.current_buffer_name eb = self.window_arrangement.get_editor_buffer_for_buffer_name(name) if eb is not None: # Run reporter. self.run_reporter_for_editor_buffer(eb) def run_reporter_for_editor_buffer(self, editor_buffer): """ Run reporter on input. (Asynchronously.) """ assert isinstance(editor_buffer, EditorBuffer) eb = editor_buffer name = eb.buffer_name if name not in self._reporters_running_for_buffer_names: text = eb.buffer.text self._reporters_running_for_buffer_names.add(name) eb.report_errors = [] # Don't run reporter when we don't have a location. (We need to # know the filetype, actually.) if eb.location is None: return # Better not to access the document in an executor. document = eb.buffer.document def in_executor(): # Call reporter report_errors = report(eb.location, document) def ready(): self._reporters_running_for_buffer_names.remove(name) # If the text has not been changed yet in the meantime, set # reporter errors. (We were running in another thread.) if text == eb.buffer.text: eb.report_errors = report_errors self.cli.invalidate() else: # Restart reporter when the text was changed. self._current_buffer_changed(self.cli) self.cli.eventloop.call_from_executor(ready) self.cli.eventloop.run_in_executor(in_executor) def show_help(self): """ Show help in new window. """ self.window_arrangement.hsplit(text=HELP_TEXT) self.sync_with_prompt_toolkit() # Show new window. def run(self): """ Run the event loop for the interface. This starts the interaction. """ # Make sure everything is in sync, before starting. self.sync_with_prompt_toolkit() # Run eventloop of prompt_toolkit. self.cli.run(reset_current_buffer=False) def enter_command_mode(self): """ Go into command mode. """ self.cli.push_focus(COMMAND_BUFFER) self.key_bindings_manager.get_vi_state(self.cli).input_mode = InputMode.INSERT self.previewer.save() def leave_command_mode(self, append_to_history=False): """ Leave command mode. Focus document window again. """ self.previewer.restore() self.cli.pop_focus() self.key_bindings_manager.get_vi_state(self.cli).input_mode = InputMode.NAVIGATION self.cli.buffers[COMMAND_BUFFER].reset(append_to_history=append_to_history)
class Editor(object): """ The main class. Containing the whole editor. """ def __init__(self, config_directory='~/.pyvim'): # Vi options. self.show_line_numbers = True self.highlight_search = True self.paste_mode = False self.show_ruler = True self.show_wildmenu = True self.expand_tab = True # Insect spaces instead of tab characters. self.tabstop = 4 # Number of spaces that a tab character represents. self.incsearch = True # Show matches while typing search string. self.ignore_case = False # Ignore case while searching. self.enable_mouse_support = True self.display_unprintable_characters = True # ':set list' self.enable_jedi = True # ':set jedi', for Python Jedi completion. self.scroll_offset = 0 # ':set scrolloff' self.relative_number = False # ':set relativenumber' self.wrap_lines = True # ':set wrap' self.cursorline = False # ':set cursorline' self.cursorcolumn = False # ':set cursorcolumn' self.colorcolumn = [] # ':set colorcolumn'. List of integers. # Ensure config directory exists. self.config_directory = os.path.abspath( os.path.expanduser(config_directory)) if not os.path.exists(self.config_directory): os.mkdir(self.config_directory) self._reporters_running_for_buffer_names = set() self.window_arrangement = WindowArrangement(self) self.message = None # Load styles. (Mapping from name to Style class.) self.styles = generate_built_in_styles() self.current_style = get_editor_style_by_name('default') # I/O backends. self.io_backends = [ DirectoryIO(), HttpIO(), GZipFileIO(), # Should come before FileIO. FileIO(), ] # Create eventloop. self.eventloop = create_eventloop() # Create key bindings registry. self.key_bindings_registry = create_key_bindings(self) # Create layout and CommandLineInterface instance. self.editor_layout = EditorLayout(self, self.window_arrangement) self.application = self._create_application() self.cli = CommandLineInterface(eventloop=self.eventloop, application=self.application) # Hide message when a key is pressed. def key_pressed(_): self.message = None self.cli.input_processor.beforeKeyPress += key_pressed # Command line previewer. self.previewer = CommandPreviewer(self) def load_initial_files(self, locations, in_tab_pages=False, hsplit=False, vsplit=False): """ Load a list of files. """ assert in_tab_pages + hsplit + vsplit <= 1 # Max one of these options. # When no files were given, open at least one empty buffer. locations2 = locations or [None] # First file self.window_arrangement.open_buffer(locations2[0]) for f in locations2[1:]: if in_tab_pages: self.window_arrangement.create_tab(f) elif hsplit: self.window_arrangement.hsplit(location=f) elif vsplit: self.window_arrangement.vsplit(location=f) else: self.window_arrangement.open_buffer(f) self.window_arrangement.active_tab_index = 0 if locations and len(locations) > 1: self.show_message('%i files loaded.' % len(locations)) def _create_application(self): """ Create CommandLineInterface instance. """ # Create Vi command buffer. def handle_action(cli, buffer): ' When enter is pressed in the Vi command line. ' text = buffer.text # Remember: leave_command_mode resets the buffer. # First leave command mode. We want to make sure that the working # pane is focussed again before executing the command handlers. self.leave_command_mode(append_to_history=True) # Execute command. handle_command(self, text) # Create history and search buffers. commands_history = FileHistory( os.path.join(self.config_directory, 'commands_history')) command_buffer = Buffer( accept_action=AcceptAction(handler=handle_action), enable_history_search=Always(), completer=create_command_completer(self), history=commands_history) search_buffer_history = FileHistory( os.path.join(self.config_directory, 'search_history')) search_buffer = Buffer(history=search_buffer_history, enable_history_search=Always(), accept_action=AcceptAction.IGNORE) # Create app. # Create CLI. application = Application( editing_mode=EditingMode.VI, layout=self.editor_layout.layout, key_bindings_registry=self.key_bindings_registry, get_title=lambda: get_terminal_title(self), buffers={ COMMAND_BUFFER: command_buffer, SEARCH_BUFFER: search_buffer, }, style=DynamicStyle(lambda: self.current_style), paste_mode=Condition(lambda cli: self.paste_mode), ignore_case=Condition(lambda cli: self.ignore_case), mouse_support=Condition(lambda cli: self.enable_mouse_support), use_alternate_screen=True, on_buffer_changed=self._current_buffer_changed) # Handle command line previews. # (e.g. when typing ':colorscheme blue', it should already show the # preview before pressing enter.) def preview(_): if self.cli.current_buffer == command_buffer: self.previewer.preview(command_buffer.text) command_buffer.on_text_changed += preview return application @property def current_editor_buffer(self): """ Return the `EditorBuffer` that is currently active. """ # For each buffer name on the focus stack. for current_buffer_name in self.cli.buffers.focus_stack: if current_buffer_name is not None: # Find/return the EditorBuffer with this name. for b in self.window_arrangement.editor_buffers: if b.buffer_name == current_buffer_name: return b @property def add_key_binding(self): """ Shortcut for adding new key bindings. (Mostly useful for a pyvimrc file, that receives this Editor instance as input.) """ return self.key_bindings_registry.add_binding def show_message(self, message): """ Set a warning message. The layout will render it as a "pop-up" at the bottom. """ self.message = message def use_colorscheme(self, name='default'): """ Apply new colorscheme. (By name.) """ try: self.current_style = get_editor_style_by_name(name) except pygments.util.ClassNotFound: pass def sync_with_prompt_toolkit(self): """ Update the prompt-toolkit Layout and FocusStack. """ # After executing a command, make sure that the layout of # prompt-toolkit matches our WindowArrangement. self.editor_layout.update() # Make sure that the focus stack of prompt-toolkit has the current # page. self.cli.focus( self.window_arrangement.active_editor_buffer.buffer_name) def _current_buffer_changed(self, cli): """ Current buffer changed. """ name = self.cli.current_buffer_name eb = self.window_arrangement.get_editor_buffer_for_buffer_name(name) if eb is not None: # Run reporter. self.run_reporter_for_editor_buffer(eb) def run_reporter_for_editor_buffer(self, editor_buffer): """ Run reporter on input. (Asynchronously.) """ assert isinstance(editor_buffer, EditorBuffer) eb = editor_buffer name = eb.buffer_name if name not in self._reporters_running_for_buffer_names: text = eb.buffer.text self._reporters_running_for_buffer_names.add(name) eb.report_errors = [] # Don't run reporter when we don't have a location. (We need to # know the filetype, actually.) if eb.location is None: return # Better not to access the document in an executor. document = eb.buffer.document def in_executor(): # Call reporter report_errors = report(eb.location, document) def ready(): self._reporters_running_for_buffer_names.remove(name) # If the text has not been changed yet in the meantime, set # reporter errors. (We were running in another thread.) if text == eb.buffer.text: eb.report_errors = report_errors self.cli.invalidate() else: # Restart reporter when the text was changed. self._current_buffer_changed(self.cli) self.cli.eventloop.call_from_executor(ready) self.cli.eventloop.run_in_executor(in_executor) def show_help(self): """ Show help in new window. """ self.window_arrangement.hsplit(text=HELP_TEXT) self.sync_with_prompt_toolkit() # Show new window. def run(self): """ Run the event loop for the interface. This starts the interaction. """ # Make sure everything is in sync, before starting. self.sync_with_prompt_toolkit() def pre_run(): # Start in navigation mode. self.cli.vi_state.input_mode = InputMode.NAVIGATION # Run eventloop of prompt_toolkit. self.cli.run(reset_current_buffer=False, pre_run=pre_run) def enter_command_mode(self): """ Go into command mode. """ self.cli.push_focus(COMMAND_BUFFER) self.cli.vi_state.input_mode = InputMode.INSERT self.previewer.save() def leave_command_mode(self, append_to_history=False): """ Leave command mode. Focus document window again. """ self.previewer.restore() self.cli.pop_focus() self.cli.vi_state.input_mode = InputMode.NAVIGATION self.cli.buffers[COMMAND_BUFFER].reset( append_to_history=append_to_history)
class Ui: repeated_message_pattern = re.compile(r'[ ]\((?P<num>[0-9]+)x\)$') def __init__(self, hook): self.hook = hook self._built = False self._scroll_state = 1 self._help_showing = False self._last_roll = None self._help_items = [] self.stat_state = {name: 0 for name in statinfo.names} self.stat_state['Rerolls'] = 0 self.stat_constraints = interactions.StatConstraintState() @property def info_wb(self): return self.info_window, self.buffers['INFO_BUFFER'] # TODO: optimize def _increment_repeated_msg(self, first=False): if first: return lambda line: line.rstrip(' ') + ' (2x)' else: incr = lambda match: f" ({str(int(match.group('num')) + 1)}x)" return lambda line: self.repeated_message_pattern.sub(incr, line) def _print(self, *args, sep=' ', pre='\n', **kwargs): new_msg = sep.join(str(x) for x in args) buffer = self.buffers['MSG_BUFFER'] last_line = buffer.document.current_line line_parts = self.repeated_message_pattern.split(last_line) last_msg = line_parts[0] if last_msg != new_msg: buffer.insert_text(pre + new_msg) else: buffer.transform_current_line( self._increment_repeated_msg(first=len(line_parts) == 1)) buffer.cursor_right(len(buffer.document.current_line)) def print(self, *args, **kwargs): if _is_main_thread(): self._print(*args, **kwargs) else: self.run_in_executor(self._print, *args, **kwargs) self.redraw() # TODO # Select the default buffer, set some text and request an input? def prompt(self, message, end='>'): ... def _update_info_text(self, buff=None): buff = buff or self.stat_buffer_state.current buffer = statinfo.Stats.get_name(buff) if buffer: buffername = buffer.name else: return text = statinfo.extra_info.get(buffername, f'TODO: {buffername} text') if text: self.set_info_text(text) self._help_showing = False def _focus(self, buffer, cli=None): cli = cli or self.cli cli.focus(buffer) self._update_info_text() # Don't question these double half scrolls this is completely correct def _scroll_up(self): if self._scroll_state < 0: scroll.scroll_half_page_up(*self.info_wb) scroll.scroll_half_page_up(*self.info_wb) scroll.scroll_half_page_up(*self.info_wb) self._scroll_state = 1 def _scroll_down(self): if self._scroll_state > 0: scroll.scroll_half_page_down(*self.info_wb) scroll.scroll_half_page_down(*self.info_wb) scroll.scroll_half_page_down(*self.info_wb) self._scroll_state = -1 def _get_window_title(self): return f"UnReal World Stat Roller V2" # Setup functions def _gen_buffers(self): self.stat_buffer_state = BufferState() return { DEFAULT_BUFFER: Buffer(initial_document=Document(""), is_multiline=False, read_only=False), 'INFO_BUFFER': Buffer(initial_document=Document(), is_multiline=True), 'MSG_BUFFER': Buffer(initial_document=Document(), is_multiline=True), **{ stat.buffername: Buffer(initial_document=make_stat_doc(stat.name)) for stat in statinfo.Stats.all_stats() } } # TODO: Lexers # from pygments.lexers import HtmlLexer # from prompt_toolkit.layout.lexers import PygmentsLexer # BufferControl(lexer=PygmentsLexer(HtmlLexer)) def _gen_layout(self): stat_windows = [] for stat_group in statinfo.groups: for stat in stat_group: stat_windows.append(make_stat_window(stat)) stat_windows.append(vpad(1)) stat_windows.append( Window(content=BufferControl(buffer_name='REROLLS_STAT_BUFFER'), **stat_args)) stat_windows.append(vpad(1)) @Condition def scroll_cond(cli): if self.info_window.render_info is None: return True try: l = self.buffers['INFO_BUFFER'].document.line_count return self.info_window.render_info.window_height < l except: return True self.info_window = Window( content=BufferControl(buffer_name='INFO_BUFFER'), dont_extend_width=True, wrap_lines=True, always_hide_cursor=True, right_margins=[ ConditionalMargin(ScrollbarMargin(display_arrows=True), scroll_cond) ]) return HSplit([ hpad(1), VSplit([ vpad(1), HSplit(stat_windows), vpad(2), # idk why there's an extra space on the stats self.info_window, vpad(1) ]), hpad(1), Window(content=BufferControl(buffer_name='MSG_BUFFER'), height=D.exact(3), wrap_lines=True), Window(content=BufferControl(buffer_name=DEFAULT_BUFFER), height=D.exact(1), always_hide_cursor=True) ]) def _gen_bindings(self): registry = Registry() bind = registry.add_binding def bind_with_help(*args, name, info='', **kwargs): def dec(func): _info = func.__doc__ or info self._help_items.append(HelpItem(name, *args, info=_info)) return bind(*args, **kwargs)(func) return dec def ensure_cursor_bounds(buffer, pos, valids=None): buffer_stat = self.stat_buffer_state.current_stat if not buffer_stat: return if valids is None: valids = self.stat_constraints.get_cursor_bounds(buffer_stat) if pos not in valids: valids = sorted(valids) pos_index = bisect.bisect_left(valids, pos) requested_pos = pos pos = valids[min(pos_index, len(valids) - 1)] # if we wind up at the same spot, check to see if there's a non-sequential spot if buffer.cursor_position == pos: moving_left = requested_pos < pos if moving_left and pos > valids[0]: pos = valids[max(0, pos_index - 1)] if not moving_left and pos < valids[-1]: pos = valids[min(pos_index + 1, len(valids) - 1)] buffer.cursor_position = pos @Condition def _in_stat_buffer(cli): return cli.current_buffer_name.endswith("_STAT_BUFFER") @Condition def _in_normal_stat_buffer(cli): return not cli.current_buffer_name.startswith( tuple(name.upper() for group in statinfo.groups[2:] for name in group)) # Navigation binds @bind(Keys.Left) @self.stat_constraints.listen def _(event): buff = event.current_buffer new_pos = buff.cursor_position + buff.document.get_cursor_left_position( count=event.arg) ensure_cursor_bounds(buff, new_pos) @bind(Keys.Right) @self.stat_constraints.listen def _(event): buff = event.current_buffer new_pos = buff.cursor_position + buff.document.get_cursor_right_position( count=event.arg) ensure_cursor_bounds(buff, new_pos) @bind(Keys.Up) @self.stat_constraints.listen def _(event): current_buffer = event.cli.current_buffer from_stat_buff = _in_normal_stat_buffer(event.cli) self._focus(self.stat_buffer_state.up()) buff = event.cli.current_buffer ensure_cursor_bounds(buff, buff.cursor_position) if _in_normal_stat_buffer(event.cli) and from_stat_buff: buff.cursor_position = current_buffer.cursor_position @bind(Keys.Down) @self.stat_constraints.listen def _(event): current_buffer = event.cli.current_buffer from_stat_buff = _in_normal_stat_buffer(event.cli) self._focus(self.stat_buffer_state.down()) buff = event.cli.current_buffer ensure_cursor_bounds(buff, buff.cursor_position) if _in_normal_stat_buffer(event.cli) and from_stat_buff: buff.cursor_position = current_buffer.cursor_position @bind(Keys.Enter, filter=_in_stat_buffer) @self.stat_constraints.listen def _(event): pass # Control binds @bind(Keys.ControlD) # @bind(Keys.ControlC) def _(event): event.cli.set_return_value(None) @bind(Keys.PageUp) def _(event): self._scroll_up() @bind(Keys.PageDown) def _(event): self._scroll_down() @bind_with_help('?', name='Help', info="Shows the help screen") def _(event): if self._help_showing: self._help_showing = False self._update_info_text() return self.set_info_text(help_text) self._help_showing = True @bind_with_help('n', name='Reroll') def _(event): l = self.reroll() @bind_with_help('y', name='Accept Stats', info="Accept current stats in game") def _(event): ... # TODO @bind_with_help('r', name='Refresh stats') def _(event): self.set_stats(**self.hook.zip(self.hook.read_all())) @bind_with_help(Keys.ControlZ, name='Undo', info="TODO: undo buffer") def _(event): self.print("I'll get to writing undo eventually") @bind_with_help(Keys.ControlY, name='Redo', info="TODO: undo buffer") def _(event): ... # TODO # Testing/Debug binds @bind_with_help('`', name='Embed IPython') def _(event): def do(): # noinspection PyStatementEffect self, event # behold the magic of closures and scope __import__('IPython').embed() os.system('cls') event.cli.run_in_terminal(do) @bind_with_help('t', name='Reroll test') def _(event): def do(): self.print("Running reroll test") num = 50 self.hook.reset_reroll_count() rrbase = self.hook._read_rerolls() t0 = time.time() for x in range(num): self.reroll() self.print("Rerolled") t1 = time.time() rrcount = self.hook._read_rerolls() - rrbase self.print(f'Rolled {num} ({rrcount}) times in {t1-t0:.4f}', 'sec') self.run_in_executor(do) @bind(',') def _(event): self.print("Showing cursor") memhook.Cursor.show() @bind('.') def _(event): self.print("Hiding cursor") memhook.Cursor.hide() @bind('-') def _(event): self.print("got random stats") self.set_stats(**memhook.get_random_stats()) return registry def _add_events(self): """ Buffer events: on_text_changed on_text_insert on_cursor_position_changed Application events: on_input_timeout on_start on_stop on_reset on_initialize on_buffer_changed on_render on_invalidate Container events: report_dimensions_callback (cli, list) """ pass def _finalize_build(self): self.set_info_text(help_text) self.print("UnReal World Stat Roller v2.0") self.print("Press ? for help\n") self._memreader = memhook.MemReader(self) self._memreader.start() memhook.Cursor.link(self.cli) def build(self): self.buffers = self._gen_buffers() self.layout = self._gen_layout() self.registry = self._gen_bindings() self._add_events() self.application = Application(layout=self.layout, buffers=self.buffers, key_bindings_registry=self.registry, get_title=self._get_window_title, mouse_support=True, use_alternate_screen=True) self.cli = CommandLineInterface(application=self.application, eventloop=create_eventloop()) self._finalize_build() self._built = True return self def run(self, build=True): if build and not self._built: self.build() elif not self._built: raise RuntimeError("UI has not been built yet") try: self.cli.run() finally: self._memreader.stop() self.cli.eventloop.close() def redraw(self): if _is_main_thread(): self.cli._redraw() else: self.cli.invalidate() def run_in_executor(self, func, *args, **kwargs): self.cli.eventloop.call_from_executor(lambda: func(*args, **kwargs)) def reroll(self): new_stats = self.hook.reroll() self.set_stats(**self.hook.zip(new_stats)) self.stat_state['Rerolls'] += 1 self.redraw() def set_stat(self, stat, value): self.stat_state[stat] = value buffer = self.buffers[statinfo.Stats.get(stat).buffername] cursor = buffer.cursor_position # If cursor position is being funky I can just set the position on the doc buffer.reset(make_stat_doc(stat, value)) buffer.cursor_position = cursor def set_stats(self, **stats): for stat, value in stats.items(): self.set_stat(stat, value) def _make_info_text(self, text): parts = str(text).strip().split('\n\n') filled_parts = [textwrap.fill(t, 35) for t in parts] return '\n\n'.join(filled_parts) def set_info_text(self, text): text = self._make_info_text(text) self.buffers['INFO_BUFFER'].reset(Document(text, cursor_position=0)) def append_info_text(self, text, sep='\n'): text = self._make_info_text(text) buffer = self.buffers['INFO_BUFFER'] newdoc = Document(buffer.document.text + sep + text, cursor_position=buffer.document.cursor_position) buffer.reset(newdoc) buffer.on_text_changed.fire() def _make_help_text(self): return help_text def on_error(self, *args): self.print(f"An error has occurred: {args[1]}") traceback.print_exception(*args)