Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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)