Example #1
0
    def test_update_response_box(self, db_dummy_data):

        # Create layout
        layout = Layout(root_container.create(),
                        focused_element=servers.content)

        summary_buffer = layout.get_buffer_by_name('summary_buffer')

        # Simulate mouse click on first server
        layout.current_window.content.text()[1][2](SingleClick())
        summary_buffer_before = json.loads(summary_buffer.text)

        # Get buttons for focus
        app = App(layout)
        ButtonManager.update_buttons(app)

        # Focus and simulate click on next server
        layout.focus(ButtonManager.buttons[1])
        layout.current_window.content.text()[1][2](SingleClick())
        summary_buffer_after = json.loads(summary_buffer.text)

        assert summary_buffer_before['name'] == 'alice'
        assert summary_buffer_after['name'] == 'bob'
Example #2
0
def text_area(title, text, lexer_name="", height=10, full_screen=False):
    """
    Small implementation of an editor/pager for small pieces of text.

    :param title: Title of the text_area
    :type  title: str
    :param text: Editable text
    :type  text: str
    :param lexer_name: If the editable text should be highlighted with
        some kind of grammar, examples are ``yaml``, ``python`` ...
    :type  lexer_name: str
    :param height: Max height of the text area
    :type  height: int
    :param full_screen: Wether or not the text area should be full screen.
    :type  full_screen: bool
    """
    from prompt_toolkit import Application
    from prompt_toolkit.enums import EditingMode
    from prompt_toolkit.buffer import Buffer
    from prompt_toolkit.layout.containers import HSplit, Window, WindowAlign
    from prompt_toolkit.layout.controls import (BufferControl,
                                                FormattedTextControl)
    from prompt_toolkit.layout.layout import Layout
    from prompt_toolkit.layout import Dimension
    from prompt_toolkit.key_binding import KeyBindings
    from prompt_toolkit.lexers import PygmentsLexer
    from pygments.lexers import find_lexer_class_by_name
    assert (type(title) == str)
    assert (type(text) == str)
    assert (type(lexer_name) == str)
    assert (type(height) == int)
    assert (type(full_screen) == bool)

    kb = KeyBindings()
    buffer1 = Buffer()
    buffer1.text = text

    @kb.add('c-q')
    def exit_(event):
        event.app.exit(0)

    @kb.add('c-s')
    def save_(event):
        event.app.return_text = buffer1.text

    class App(Application):
        return_text = None

    text_height = Dimension(min=0, max=height) if height is not None else None

    pygment_lexer = find_lexer_class_by_name(lexer_name)
    lexer = PygmentsLexer(pygment_lexer)
    text_window = Window(height=text_height,
                         content=BufferControl(buffer=buffer1, lexer=lexer))

    root_container = HSplit([
        Window(char='-',
               align=WindowAlign.CENTER,
               height=1,
               content=FormattedTextControl(text=[('fg:ansiblack bg:ansiwhite',
                                                   title)]),
               always_hide_cursor=True),
        text_window,
        Window(height=1,
               width=None,
               align=WindowAlign.CENTER,
               char='-',
               content=FormattedTextControl(
                   text=[('fg:ansiblack bg:ansiwhite',
                          "Quit [Ctrl-q]  Save [Ctrl-s]")])),
    ])

    layout = Layout(root_container)

    layout.focus(text_window)

    app = App(editing_mode=(EditingMode.EMACS if papis.config.get(
        'editmode', section='tui') == 'emacs' else EditingMode.VI),
              layout=layout,
              key_bindings=kb,
              full_screen=full_screen)
    app.run()
    return app.return_text
Example #3
0
class NumberPrompt(BaseComplexPrompt):
    """Create a input prompts that only takes number as input.

    A wrapper class around :class:`~prompt_toolkit.application.Application`.

    Args:
        message: The question to ask the user.
            Refer to :ref:`pages/dynamic:message` documentation for more details.
        style: An :class:`InquirerPyStyle` instance.
            Refer to :ref:`Style <pages/style:Alternate Syntax>` documentation for more details.
        vi_mode: Use vim keybinding for the prompt.
            Refer to :ref:`pages/kb:Keybindings` documentation for more details.
        default: Set the default value of the prompt.
            You can enter either the floating value or integer value as the default.
            Refer to :ref:`pages/dynamic:default` documentation for more details.
        float_allowed: Allow decimal input. This will change the prompt to have 2 input buffer, one for the
            whole value and one for the integral value.
        min_allowed: Set the minimum value of the prompt. When the input value goes below this value, it
            will automatically reset to this value.
        max_allowed: Set the maximum value of the prompt. When the inptu value goes above this value, it
            will automatically reset to this value.
        qmark: Question mark symbol. Custom symbol that will be displayed infront of the question before its answered.
        amark: Answer mark symbol. Custom symbol that will be displayed infront of the question after its answered.
        decimal_symbol: Decimal point symbol. Custom symbol to display as the decimal point.
        replace_mode: Start each input buffer in replace mode if default value is 0.
            When typing, it will replace the 0 with the new value. The replace mode will be disabled once the value
            is changed.
        instruction: Short instruction to display next to the question.
        long_instruction: Long instructions to display at the bottom of the prompt.
        validate: Add validation to user input.
            Refer to :ref:`pages/validator:Validator` documentation for more details.
        invalid_message: Error message to display when user input is invalid.
            Refer to :ref:`pages/validator:Validator` documentation for more details.
        invalid_message: Error message to display when user input is invalid.
            Refer to :ref:`pages/validator:Validator` documentation for more details.
        transformer: A function which performs additional transformation on the value that gets printed to the terminal.
            Different than `filter` parameter, this is only visual effect and won’t affect the actual value returned by :meth:`~InquirerPy.base.simple.BaseSimplePrompt.execute`.
            Refer to :ref:`pages/dynamic:transformer` documentation for more details.
        filter: A function which performs additional transformation on the result.
            This affects the actual value returned by :meth:`~InquirerPy.base.simple.BaseSimplePrompt.execute`.
            Refer to :ref:`pages/dynamic:filter` documentation for more details.
        keybindings: Customise the builtin keybindings.
            Refer to :ref:`pages/kb:Keybindings` for more details.
        wrap_lines: Soft wrap question lines when question exceeds the terminal width.
        raise_keyboard_interrupt: Raise the :class:`KeyboardInterrupt` exception when `ctrl-c` is pressed. If false, the result
            will be `None` and the question is skiped.
        mandatory: Indicate if the prompt is mandatory. If True, then the question cannot be skipped.
        mandatory_message: Error message to show when user attempts to skip mandatory prompt.
        session_result: Used internally for :ref:`index:Classic Syntax (PyInquirer)`.

    Examples:
        >>> from InquirerPy import inquirer
        >>> result = inquirer.number(message="Enter number:").execute()
        >>> print(result)
        0
    """
    def __init__(
        self,
        message: InquirerPyMessage,
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        default: InquirerPyDefault = 0,
        float_allowed: bool = False,
        max_allowed: Union[int, float] = None,
        min_allowed: Union[int, float] = None,
        decimal_symbol: str = ". ",
        replace_mode: bool = False,
        qmark: str = INQUIRERPY_QMARK_SEQUENCE,
        amark: str = "?",
        instruction: str = "",
        long_instruction: str = "",
        validate: InquirerPyValidate = None,
        invalid_message: str = "Invalid input",
        transformer: Callable[[str], Any] = None,
        filter: Callable[[str], Any] = None,
        keybindings: InquirerPyKeybindings = None,
        wrap_lines: bool = True,
        raise_keyboard_interrupt: bool = True,
        mandatory: bool = True,
        mandatory_message: str = "Mandatory prompt",
        session_result: InquirerPySessionResult = None,
    ) -> None:
        super().__init__(
            message=message,
            style=style,
            vi_mode=vi_mode,
            qmark=qmark,
            amark=amark,
            transformer=transformer,
            filter=filter,
            invalid_message=invalid_message,
            validate=validate,
            instruction=instruction,
            long_instruction=long_instruction,
            wrap_lines=wrap_lines,
            raise_keyboard_interrupt=raise_keyboard_interrupt,
            mandatory=mandatory,
            mandatory_message=mandatory_message,
            session_result=session_result,
        )

        self._float = float_allowed
        self._is_float = Condition(lambda: self._float)
        self._max = max_allowed
        self._min = min_allowed
        self._value_error_message = "Remove any non-integer value"
        self._decimal_symbol = decimal_symbol
        self._whole_replace = False
        self._integral_replace = False
        self._replace_mode = replace_mode

        self._leading_zero_pattern = re.compile(r"^(0*)[0-9]+.*")
        self._sn_pattern = re.compile(r"^.*E-.*")
        self._no_default = False

        if default is None:
            default = 0
            self._no_default = True

        if isinstance(default, Callable):
            default = cast(Callable, default)(session_result)
        if self._float:
            default = Decimal(str(float(cast(int, default))))
        if self._float:
            if not isinstance(default, float) and not isinstance(
                    default, Decimal):
                raise InvalidArgument(
                    f"{type(self).__name__} argument 'default' should return type of float or Decimal"
                )
        elif not isinstance(default, int):
            raise InvalidArgument(
                f"{type(self).__name__} argument 'default' should return type of int"
            )
        self._default = default

        if keybindings is None:
            keybindings = {}
        self.kb_maps = {
            "down": [
                {
                    "key": "down"
                },
                {
                    "key": "c-n",
                    "filter": ~self._is_vim_edit
                },
                {
                    "key": "j",
                    "filter": self._is_vim_edit
                },
            ],
            "up": [
                {
                    "key": "up"
                },
                {
                    "key": "c-p",
                    "filter": ~self._is_vim_edit
                },
                {
                    "key": "k",
                    "filter": self._is_vim_edit
                },
            ],
            "left": [
                {
                    "key": "left"
                },
                {
                    "key": "c-b",
                    "filter": ~self._is_vim_edit
                },
                {
                    "key": "h",
                    "filter": self._is_vim_edit
                },
            ],
            "right": [
                {
                    "key": "right"
                },
                {
                    "key": "c-f",
                    "filter": ~self._is_vim_edit
                },
                {
                    "key": "l",
                    "filter": self._is_vim_edit
                },
            ],
            "dot": [{
                "key": "."
            }],
            "focus": [{
                "key": Keys.Tab
            }, {
                "key": "s-tab"
            }],
            "input": [{
                "key": str(i)
            } for i in range(10)],
            "negative_toggle": [{
                "key": "-"
            }],
            **keybindings,
        }
        self.kb_func_lookup = {
            "down": [{
                "func": self._handle_down
            }],
            "up": [{
                "func": self._handle_up
            }],
            "left": [{
                "func": self._handle_left
            }],
            "right": [{
                "func": self._handle_right
            }],
            "focus": [{
                "func": self._handle_focus
            }],
            "input": [{
                "func": self._handle_input
            }],
            "negative_toggle": [{
                "func": self._handle_negative_toggle
            }],
            "dot": [{
                "func": self._handle_dot
            }],
        }

        @self.register_kb(Keys.Any)
        def _(_):
            pass

        self._whole_width = 1
        self._whole_buffer = Buffer(
            on_text_changed=self._on_whole_text_change,
            on_cursor_position_changed=self._on_cursor_position_change,
        )

        self._integral_width = 1
        self._integral_buffer = Buffer(
            on_text_changed=self._on_integral_text_change,
            on_cursor_position_changed=self._on_cursor_position_change,
        )

        self._whole_window = Window(
            height=LayoutDimension.exact(1) if not self._wrap_lines else None,
            content=BufferControl(
                buffer=self._whole_buffer,
                lexer=SimpleLexer("class:input"),
            ),
            width=lambda: Dimension(
                min=self._whole_width,
                max=self._whole_width,
                preferred=self._whole_width,
            ),
            dont_extend_width=True,
        )

        self._integral_window = Window(
            height=LayoutDimension.exact(1) if not self._wrap_lines else None,
            content=BufferControl(
                buffer=self._integral_buffer,
                lexer=SimpleLexer("class:input"),
            ),
            width=lambda: Dimension(
                min=self._integral_width,
                max=self._integral_width,
                preferred=self._integral_width,
            ),
        )

        self._layout = Layout(
            HSplit([
                VSplit(
                    [
                        Window(
                            height=LayoutDimension.exact(1)
                            if not self._wrap_lines else None,
                            content=FormattedTextControl(
                                self._get_prompt_message),
                            wrap_lines=self._wrap_lines,
                            dont_extend_height=True,
                            dont_extend_width=True,
                        ),
                        ConditionalContainer(self._whole_window,
                                             filter=~IsDone()),
                        ConditionalContainer(
                            Window(
                                height=LayoutDimension.exact(1)
                                if not self._wrap_lines else None,
                                content=FormattedTextControl(
                                    [("", self._decimal_symbol)]),
                                wrap_lines=self._wrap_lines,
                                dont_extend_height=True,
                                dont_extend_width=True,
                            ),
                            filter=self._is_float & ~IsDone(),
                        ),
                        ConditionalContainer(
                            self._integral_window,
                            filter=self._is_float & ~IsDone()),
                    ],
                    align=HorizontalAlign.LEFT,
                ),
                ConditionalContainer(
                    Window(content=DummyControl()),
                    filter=~IsDone() & self._is_displaying_long_instruction,
                ),
                ValidationWindow(
                    invalid_message=self._get_error_message,
                    filter=self._is_invalid & ~IsDone(),
                    wrap_lines=self._wrap_lines,
                ),
                InstructionWindow(
                    message=self._long_instruction,
                    filter=~IsDone() & self._is_displaying_long_instruction,
                    wrap_lines=self._wrap_lines,
                ),
            ]), )

        self.focus = self._whole_window

        self._application = Application(
            layout=self._layout,
            style=self._style,
            key_bindings=self._kb,
            after_render=self._after_render,
            editing_mode=self._editing_mode,
        )

    def _fix_sn(self, value: str) -> Tuple[str, str]:
        """Fix sciencetific notation format.

        Args:
            value: Value to fix.

        Returns:
            A tuple of whole buffer text and integral buffer text.
        """
        left, right = value.split("E-")
        whole_buffer_text = "0"
        integral_buffer_text = f"{(int(right) - 1) * '0'}{left.replace('.', '')}"
        return whole_buffer_text, integral_buffer_text

    def _on_rendered(self, _) -> None:
        """Additional processing to adjust buffer content after render."""
        if self._no_default:
            return
        if not self._float:
            self._whole_buffer.text = str(self._default)
            self._integral_buffer.text = "0"
        else:
            if self._sn_pattern.match(str(self._default)) is None:
                whole_buffer_text, integral_buffer_text = str(
                    self._default).split(".")
            else:
                whole_buffer_text, integral_buffer_text = self._fix_sn(
                    str(self._default))
            self._integral_buffer.text = integral_buffer_text
            self._whole_buffer.text = whole_buffer_text
        self._whole_buffer.cursor_position = len(self._whole_buffer.text)
        self._integral_buffer.cursor_position = len(self._integral_buffer.text)
        if self._replace_mode:
            # check to start replace mode if applicable
            if self._whole_buffer.text == "0":
                self._whole_replace = True
                self._whole_buffer.cursor_position = 0
            if self._integral_buffer.text == "0":
                self._integral_replace = True
                self._integral_buffer.cursor_position = 0

    def _handle_number(self, increment: bool) -> None:
        """Handle number increment and decrement.

        Additional processing to handle leading zeros in integral buffer
        as well as SN notation.

        Args:
            increment: Indicate if the operation should increment or decrement.
        """
        if self.buffer_replace:
            self.buffer_replace = False
            self.focus_buffer.cursor_position += 1
        try:
            leading_zeros = ""
            if self.focus_buffer == self._integral_buffer:
                zeros = self._leading_zero_pattern.match(
                    self._integral_buffer.text)
                if zeros is not None:
                    leading_zeros = zeros.group(1)
            current_text_len = len(self.focus_buffer.text)
            if not self.focus_buffer.text:
                next_text = "0"
                next_text_len = 1
            else:
                if not increment:
                    if (self.focus_buffer == self._integral_buffer
                            and int(self.focus_buffer.text) == 0):
                        return
                    next_text = leading_zeros + str(
                        int(self.focus_buffer.text) - 1)
                else:
                    next_text = leading_zeros + str(
                        int(self.focus_buffer.text) + 1)
                next_text_len = len(next_text)
            desired_position = (self.focus_buffer.cursor_position +
                                next_text_len - current_text_len)
            self.focus_buffer.cursor_position = desired_position
            self.focus_buffer.text = next_text
            if self.focus_buffer.cursor_position != desired_position:
                self.focus_buffer.cursor_position = desired_position
        except ValueError:
            self._set_error(message=self._value_error_message)

    def _handle_down(self, _) -> None:
        """Handle down key press."""
        self._handle_number(increment=False)

    def _handle_up(self, _) -> None:
        """Handle up key press."""
        self._handle_number(increment=True)

    def _handle_left(self, _) -> None:
        """Handle left key press.

        Move to the left by one cursor position and focus the whole window
        if applicable.
        """
        self.buffer_replace = False
        if (self.focus == self._integral_window
                and self.focus_buffer.cursor_position == 0):
            self.focus = self._whole_window
        else:
            self.focus_buffer.cursor_position -= 1

    def _handle_right(self, _) -> None:
        """Handle right key press.

        Move to the right by one cursor position and focus the integral window
        if applicable.
        """
        self.buffer_replace = False
        if (self.focus == self._whole_window
                and self.focus_buffer.cursor_position == len(
                    self.focus_buffer.text) and self._float):
            self.focus = self._integral_window
        else:
            self.focus_buffer.cursor_position += 1

    def _handle_enter(self, event) -> None:
        """Handle enter event and answer/close the prompt."""
        if not self._float and not self._whole_buffer.text:
            result = ""
        elif (self._float and not self._whole_buffer.text
              and not self._integral_buffer.text):
            result = ""
        else:
            result = str(self.value)

        try:
            fake_document = FakeDocument(result)
            self._validator.validate(fake_document)  # type: ignore
        except ValidationError as e:
            self._set_error(str(e))
        else:
            self.status["answered"] = True
            self.status["result"] = result
            event.app.exit(result=result)

    def _handle_dot(self, _) -> None:
        """Focus the integral window if `float_allowed`."""
        self._handle_focus(_, self._integral_window)

    def _handle_focus(self, _, window: Window = None) -> None:
        """Focus either the integral window or whole window."""
        if not self._float:
            return
        if window is not None:
            self.focus = window
            return
        if self.focus == self._whole_window:
            self.focus = self._integral_window
        else:
            self.focus = self._whole_window

    def _handle_input(self, event: "KeyPressEvent") -> None:
        """Handle user input of numbers.

        Buffer will start as replace mode if the value is zero, once
        cursor is moved or content is changed, disable replace mode.
        """
        if self.buffer_replace:
            self.buffer_replace = False
            self.focus_buffer.text = event.key_sequence[0].data
            self.focus_buffer.cursor_position += 1
        else:
            self.focus_buffer.insert_text(event.key_sequence[0].data)

    def _handle_negative_toggle(self, _) -> None:
        """Toggle negativity of the prompt value.

        Force the `-` sign at the start.
        """
        if self._whole_buffer.text == "-":
            self._whole_buffer.text = "0"
            return
        if self._whole_buffer.text.startswith("-"):
            move_cursor = self._whole_buffer.cursor_position < len(
                self._whole_buffer.text)
            self._whole_buffer.text = self._whole_buffer.text[1:]
            if move_cursor:
                self._whole_buffer.cursor_position -= 1
        else:
            move_cursor = self._whole_buffer.cursor_position != 0
            self._whole_buffer.text = f"-{self._whole_buffer.text}"
            if move_cursor:
                self._whole_buffer.cursor_position += 1

    def _on_whole_text_change(self, buffer: Buffer) -> None:
        """Handle event of text changes in buffer."""
        self._whole_width = len(buffer.text) + 1
        self._on_text_change(buffer)

    def _on_integral_text_change(self, buffer: Buffer) -> None:
        """Handle event of text changes in buffer."""
        self._integral_width = len(buffer.text) + 1
        self._on_text_change(buffer)

    def _on_text_change(self, buffer: Buffer) -> None:
        """Disable replace mode and fix cursor position on text changes."""
        self.buffer_replace = False
        if buffer.text and buffer.text != "-":
            self.value = self.value
        if buffer.text.startswith("-") and buffer.cursor_position == 0:
            buffer.cursor_position = 1

    def _on_cursor_position_change(self, buffer: Buffer) -> None:
        """Fix cursor position on cursor movement."""
        if self.focus_buffer.text.startswith(
                "-") and buffer.cursor_position == 0:
            buffer.cursor_position = 1

    @property
    def buffer_replace(self) -> bool:
        """bool: Current buffer replace mode."""
        if self.focus_buffer == self._whole_buffer:
            return self._whole_replace
        else:
            return self._integral_replace

    @buffer_replace.setter
    def buffer_replace(self, value) -> None:
        if self.focus_buffer == self._whole_buffer:
            self._whole_replace = value
        else:
            self._integral_replace = value

    @property
    def focus_buffer(self) -> Buffer:
        """Buffer: Current editable buffer."""
        if self.focus == self._whole_window:
            return self._whole_buffer
        else:
            return self._integral_buffer

    @property
    def focus(self) -> Window:
        """Window: Current focused window."""
        return self._focus

    @focus.setter
    def focus(self, value: Window) -> None:
        self._focus = value
        self._layout.focus(self._focus)

    @property
    def value(self) -> Union[int, float, Decimal]:
        """Union[int, float]: The actual value of the prompt, combining and transforming all input buffer values."""
        try:
            if not self._float:
                return int(self._whole_buffer.text)
            else:
                return Decimal(
                    f"{self._whole_buffer.text}.{self._integral_buffer.text if self._integral_buffer.text else 0}"
                )
        except ValueError:
            self._set_error(self._value_error_message)
            return self._default

    @value.setter
    def value(self, value: Union[int, float, Decimal]) -> None:
        if self._min is not None:
            value = max(
                value,
                self._min if not self._float else Decimal(str(self._min)))
        if self._max is not None:
            value = min(
                value,
                self._max if not self._float else Decimal(str(self._max)))
        if not self._float:
            self._whole_buffer.text = str(value)
        else:
            if self._sn_pattern.match(str(value)) is None:
                whole_buffer_text, integral_buffer_text = str(value).split(".")
            else:
                whole_buffer_text, integral_buffer_text = self._fix_sn(
                    str(value))

            if self._whole_buffer.text:
                self._whole_buffer.text = whole_buffer_text
            if self._integral_buffer.text:
                self._integral_buffer.text = integral_buffer_text
Example #4
0
 def __enter__(self) -> Application:
     """Build a Layout and instantiate an Application around it."""
     main = VSplit(
         (
             # Command History on most of the left panel, Prompt at the bottom.
             HSplit(
                 (
                     self.terminal,
                     ConditionalContainer(
                         Window(  # Command Prompt.
                             BufferControl(self.command_buffer, self.procs),
                             dont_extend_height=True,
                             wrap_lines=True,
                         ),
                         Condition(lambda: not self.busy()),
                     ),
                     ConditionalContainer(
                         Window(  # "Busy" Prompt, blocks Commands.
                             FormattedTextControl("..."),
                             height=1,
                             ignore_content_width=True,
                         ),
                         Condition(self.busy),
                     ),
                     ConditionalContainer(
                         Window(  # Completion Bar.
                             FormattedTextControl(lambda: self.handler.completion),
                             height=1,
                             ignore_content_width=True,
                             style=self.style_meth,
                         ),
                         Condition(lambda: bool(self.handler.completion)),
                     ),
                 )
             ),
             ConditionalContainer(  # Vertical Line.
                 HSplit(
                     (
                         VerticalLine(),
                         Window(
                             FormattedTextControl(
                                 lambda: "├" if self.state is Mode.SCOPES else "│"
                             ),
                             width=1,
                             height=1,
                         ),
                         VerticalLine(),
                     )
                 ),
                 Condition(lambda: self.state is not Mode.OFF),
             ),
             ConditionalContainer(  # Scopes Panel. Visualizes nearby Space.
                 HSplit(
                     (
                         # Top-down visualization on the upper panel.
                         Window(
                             self.scope_topdown,
                             ignore_content_height=True,
                             ignore_content_width=True,
                         ),
                         HorizontalLine(),
                         # Visualization from behind on the lower panel.
                         Window(
                             self.scope_horizon,
                             ignore_content_height=True,
                             ignore_content_width=True,
                         ),
                     )
                 ),
                 Condition(lambda: self.state is Mode.SCOPES),
             ),
             ConditionalContainer(  # Scans Panel. Lists nearby Objects.
                 Window(self.scans, ignore_content_width=True),
                 Condition(lambda: self.state is Mode.SCANS),
             ),
             ConditionalContainer(  # Orders Panel. Shows future actions.
                 Window(self.orders, ignore_content_width=True),
                 Condition(lambda: self.state is Mode.ORDERS),
             ),
         )
     )
     root = Layout(
         HSplit(
             (
                 Window(self.header_bar, height=1, style=self.style_meth),
                 FloatContainer(main, self.floating_elems),
             )
         )
     )
     root.focus(self.command_buffer)
     self._app = Application(root, STYLE, full_screen=True, key_bindings=self.kb)
     return self._app
Example #5
0
class FullNodeUI:
    """
    Full node UI instance. Displays node state, blocks, and connections. Calls parent_close_cb
    when the full node is closed. Uses store, blockchain, and connections, to display relevant
    information. The UI is updated periodically.
    """
    def __init__(
        self,
        store: FullNodeStore,
        blockchain: Blockchain,
        server: ChiaServer,
        port: int,
        parent_close_cb: Callable,
    ):
        self.port: int = port
        self.store: FullNodeStore = store
        self.blockchain: Blockchain = blockchain
        self.node_server: ChiaServer = server
        self.connections: PeerConnections = server.global_connections
        self.logs: List[logging.LogRecord] = []
        self.app: Optional[Application] = None
        self.closed: bool = False
        self.num_blocks: int = 10
        self.num_top_block_pools: int = 5
        self.top_winners: List[Tuple[uint64, bytes32]] = []
        self.our_winners: List[Tuple[uint64, bytes32]] = []
        self.prev_route: str = "home/"
        self.route: str = "home/"
        self.focused: bool = False
        self.parent_close_cb = parent_close_cb
        self.kb = self.setup_keybindings()
        self.style = Style([("error", "#ff0044")])
        self.pool_pks: List[PublicKey] = []
        key_config_filename = os.path.join(ROOT_DIR, "config", "keys.yaml")
        if os.path.isfile(key_config_filename):
            config = safe_load(open(key_config_filename, "r"))

            self.pool_pks = [
                PrivateKey.from_bytes(bytes.fromhex(ce)).get_public_key()
                for ce in config["pool_sks"]
            ]

        self.draw_initial()
        self.app = Application(
            style=self.style,
            layout=self.layout,
            full_screen=True,
            key_bindings=self.kb,
            mouse_support=True,
        )

        self.closed = False
        self.update_ui_task = asyncio.get_running_loop().create_task(
            self.update_ui())
        self.update_data_task = asyncio.get_running_loop().create_task(
            self.update_data())

    def close(self):
        # Closes this instance of the UI
        if not self.closed:
            self.closed = True
            self.route = "home/"
            if self.app:
                self.app.exit(0)

    def stop(self):
        # Closes this instance of the UI, and call parent close, which closes
        # all other instances, and shuts down the full node.
        self.close()
        self.parent_close_cb()

    def setup_keybindings(self) -> KeyBindings:
        kb = KeyBindings()
        kb.add("tab")(focus_next)
        kb.add("s-tab")(focus_previous)
        kb.add("down")(focus_next)
        kb.add("up")(focus_previous)
        kb.add("right")(focus_next)
        kb.add("left")(focus_previous)

        @kb.add("c-c")
        def exit_(event):
            self.close()

        return kb

    def draw_initial(self):
        search_field = SearchToolbar()
        self.empty_row = TextArea(focusable=False, height=1)

        # home/
        self.loading_msg = Label(text=f"Initializing UI....")
        self.server_msg = Label(text=f"Server running on port {self.port}.")
        self.syncing = TextArea(focusable=False, height=1)
        self.current_heads_label = TextArea(focusable=False, height=1)
        self.lca_label = TextArea(focusable=False, height=1)
        self.difficulty_label = TextArea(focusable=False, height=1)
        self.ips_label = TextArea(focusable=False, height=1)
        self.total_iters_label = TextArea(focusable=False, height=2)
        self.con_rows = []
        self.displayed_cons = []
        self.latest_blocks: List[HeaderBlock] = []
        self.connections_msg = Label(text=f"Connections")
        self.connection_rows_vsplit = Window()
        self.add_connection_msg = Label(text=f"Add a connection ip:port")
        self.add_connection_field = TextArea(
            height=1,
            prompt=">>> ",
            style="class:input-field",
            multiline=False,
            wrap_lines=False,
            search_field=search_field,
        )
        self.add_connection_field.accept_handler = self.async_to_sync(
            self.add_connection)
        self.latest_blocks_msg = Label(text=f"Latest blocks")
        self.latest_blocks_labels = [
            Button(text="block") for _ in range(self.num_blocks)
        ]

        self.search_block_msg = Label(text=f"Search block by hash")
        self.search_block_field = TextArea(
            height=1,
            prompt=">>> ",
            style="class:input-field",
            multiline=False,
            wrap_lines=False,
            search_field=search_field,
        )
        self.search_block_field.accept_handler = self.async_to_sync(
            self.search_block)

        self.top_block_pools_msg = Label(text=f"Top block pools")
        self.top_block_pools_labels = [
            Label(text="Top block pool")
            for _ in range(self.num_top_block_pools)
        ]
        self.our_pools_msg = Label(text=f"Our pool winnings")
        self.our_pools_labels = [
            Label(text="Our winnings") for _ in range(len(self.pool_pks))
        ]

        self.close_ui_button = Button("Close UI", handler=self.close)
        self.quit_button = Button("Stop node and close UI", handler=self.stop)
        self.error_msg = Label(style="class:error", text=f"")

        # block/
        self.block_msg = Label(text=f"Block")
        self.block_label = TextArea(focusable=True,
                                    scrollbar=True,
                                    focus_on_click=True)
        self.back_button = Button(text="Back",
                                  handler=self.change_route_handler("home/"))
        self.challenge_msg = Label(text=f"Block Header")
        self.challenge = TextArea(focusable=False)

        body = HSplit([self.loading_msg, self.server_msg],
                      height=D(),
                      width=D())
        self.content = Frame(title="Chia Full Node", body=body)
        self.layout = Layout(VSplit([self.content], height=D(), width=D()))

    def change_route_handler(self, route):
        def change_route():
            self.prev_route = self.route
            self.route = route
            self.focused = False
            self.error_msg.text = ""

        return change_route

    def async_to_sync(self, coroutine):
        def inner(buff):
            asyncio.get_running_loop().create_task(coroutine(buff.text))

        return inner

    async def search_block(self, text: str):
        try:
            block = await self.store.get_block(bytes.fromhex(text))
        except ValueError:
            self.error_msg.text = "Enter a valid hex block hash"
            return
        if block is not None:
            self.change_route_handler(f"block/{text}")()
        else:
            self.error_msg.text = "Block not found"

    async def add_connection(self, text: str):
        if ":" not in text:
            self.error_msg.text = (
                "Enter a valid IP and port in the following format: 10.5.4.3:8000"
            )
            return
        else:
            ip, port = ":".join(text.split(":")[:-1]), text.split(":")[-1]
        target_node: PeerInfo = PeerInfo(ip, uint16(int(port)))
        log.error(f"Want to connect to {ip}, {port}")
        if not (await self.node_server.start_client(target_node, None)):
            self.error_msg.text = f"Failed to connect to {ip}:{port}"

    async def get_latest_blocks(self,
                                heads: List[HeaderBlock]) -> List[HeaderBlock]:
        added_blocks: List[HeaderBlock] = []
        while len(added_blocks) < self.num_blocks and len(heads) > 0:
            heads = sorted(heads, key=lambda b: b.height, reverse=True)
            max_block = heads[0]
            if max_block not in added_blocks:
                added_blocks.append(max_block)
            heads.remove(max_block)
            prev: Optional[HeaderBlock] = self.blockchain.header_blocks.get(
                max_block.prev_header_hash, None)
            if prev is not None:
                heads.append(prev)
        return added_blocks

    async def draw_home(self):
        connections = [c for c in self.connections.get_connections()]
        if collections.Counter(connections) != collections.Counter(
                self.displayed_cons):
            new_con_rows = []
            for con in connections:
                con_str = f"{NodeType(con.connection_type).name} {con.get_peername()} {con.node_id.hex()[:10]}..."
                con_label = Label(text=con_str)

                def disconnect(c):
                    def inner():
                        self.connections.close(c)
                        self.layout.focus(self.quit_button)

                    return inner

                disconnect_button = Button("Disconnect",
                                           handler=disconnect(con))
                row = VSplit([con_label, disconnect_button])
                new_con_rows.append(row)
            self.displayed_cons = connections
            self.con_rows = new_con_rows
            if len(self.con_rows) > 0:
                self.layout.focus(self.con_rows[0])
            else:
                self.layout.focus(self.quit_button)

        if len(self.con_rows):
            new_con_rows = HSplit(self.con_rows)
        else:
            new_con_rows = Window(width=D(), height=0)

        if await self.store.get_sync_mode():
            max_height = -1
            for _, block in await self.store.get_potential_tips_tuples():
                if block.height > max_height:
                    max_height = block.height

            if max_height >= 0:
                self.syncing.text = f"Syncing up to {max_height}"
            else:
                self.syncing.text = f"Syncing"
        else:
            self.syncing.text = "Not syncing"
        heads: List[HeaderBlock] = self.blockchain.get_current_tips()

        lca_block: FullBlock = self.blockchain.lca_block
        if lca_block.height > 0:
            difficulty = await self.blockchain.get_next_difficulty(
                lca_block.prev_header_hash)
            ips = await self.blockchain.get_next_ips(lca_block.prev_header_hash
                                                     )
        else:
            difficulty = await self.blockchain.get_next_difficulty(
                lca_block.header_hash)
            ips = await self.blockchain.get_next_ips(lca_block.header_hash)
        total_iters = lca_block.header_block.challenge.total_iters

        new_block_labels = []
        for i, b in enumerate(self.latest_blocks):
            self.latest_blocks_labels[i].text = (
                f"{b.height}:{b.header_hash}"
                f" {'LCA' if b.header_hash == lca_block.header_hash else ''}"
                f" {'TIP' if b.header_hash in [h.header_hash for h in heads] else ''}"
            )
            self.latest_blocks_labels[i].handler = self.change_route_handler(
                f"block/{b.header_hash}")
            new_block_labels.append(self.latest_blocks_labels[i])

        top_block_pools_labels = self.top_block_pools_labels
        if len(self.top_winners) > 0:
            new_top_block_pools_labels = []
            for i, (winnings, pk) in enumerate(self.top_winners):
                self.top_block_pools_labels[
                    i].text = f"Public key {pk.hex()}: {winnings/1000000000000} chias."
                new_top_block_pools_labels.append(
                    self.top_block_pools_labels[i])
            top_block_pools_labels = new_top_block_pools_labels

        our_pools_labels = self.our_pools_labels
        if len(self.our_winners) > 0:
            new_our_pools_labels = []
            for i, (winnings, pk) in enumerate(self.our_winners):
                self.our_pools_labels[
                    i].text = f"Public key {pk.hex()}: {winnings/(1000000000000)} chias."
                new_our_pools_labels.append(self.our_pools_labels[i])
            our_pools_labels = new_our_pools_labels

        self.lca_label.text = f"Current least common ancestor {lca_block.header_hash} height {lca_block.height}"
        self.current_heads_label.text = "Heights of tips: " + str(
            [h.height for h in heads])
        self.difficulty_label.text = f"Current difficuty: {difficulty}"
        self.ips_label.text = f"Current VDF iterations per second: {ips}"
        self.total_iters_label.text = f"Total iterations since genesis: {total_iters}"

        try:
            if not self.focused:
                self.layout.focus(self.close_ui_button)
                self.focused = True
        except ValueError:  # Not yet in layout
            pass
        return HSplit(
            [
                self.server_msg,
                self.syncing,
                self.lca_label,
                self.current_heads_label,
                self.difficulty_label,
                self.ips_label,
                self.total_iters_label,
                Window(height=1, char="-", style="class:line"),
                self.connections_msg,
                new_con_rows,
                Window(height=1, char="-", style="class:line"),
                self.add_connection_msg,
                self.add_connection_field,
                Window(height=1, char="-", style="class:line"),
                self.latest_blocks_msg,
                *new_block_labels,
                Window(height=1, char="-", style="class:line"),
                self.search_block_msg,
                self.search_block_field,
                Window(height=1, char="-", style="class:line"),
                self.top_block_pools_msg,
                *top_block_pools_labels,
                Window(height=1, char="-", style="class:line"),
                self.our_pools_msg,
                *our_pools_labels,
                Window(height=1, char="-", style="class:line"),
                self.close_ui_button,
                self.quit_button,
                self.error_msg,
            ],
            width=D(),
            height=D(),
        )

    async def draw_block(self):
        block_hash: str = self.route.split("block/")[1]
        async with self.store.lock:
            block: Optional[FullBlock] = await self.store.get_block(
                bytes32(bytes.fromhex(block_hash)))
        if block is not None:
            self.block_msg.text = f"Block {str(block.header_hash)}"
            if self.block_label.text != str(block):
                self.block_label.text = str(block)
        else:
            self.block_label.text = f"Block hash {block_hash} not found"
        try:
            if not self.focused:
                self.layout.focus(self.back_button)
                self.focused = True
        except ValueError:  # Not yet in layout
            pass
        return HSplit([self.block_msg, self.block_label, self.back_button],
                      width=D(),
                      height=D())

    async def update_ui(self):
        try:
            while not self.closed:
                if self.route.startswith("home/"):
                    self.content.body = await self.draw_home()
                elif self.route.startswith("block/"):
                    self.content.body = await self.draw_block()

                if self.app and not self.app.invalidated:
                    self.app.invalidate()
                await asyncio.sleep(0.25)
        except concurrent.futures._base.CancelledError as e:
            log.warn(f"Cancelled error in UI: {type(e)}: {e}")
        except Exception as e:
            log.warn(f"Exception in UI update_ui {type(e)}: {e}")
            raise e

    async def update_data(self):
        try:
            while not self.closed:
                heads: List[HeaderBlock] = self.blockchain.get_current_tips()
                self.latest_blocks = await self.get_latest_blocks(heads)

                header_block = heads[0]
                coin_balances = {
                    bytes(header_block.proof_of_space.pool_pubkey):
                    calculate_block_reward(header_block.height)
                }
                while header_block.height != 0:
                    header_block = self.blockchain.header_blocks[
                        header_block.prev_header_hash]
                    pool_pk = bytes(header_block.proof_of_space.pool_pubkey)
                    if pool_pk not in coin_balances:
                        coin_balances[pool_pk] = 0
                    coin_balances[pool_pk] += calculate_block_reward(
                        header_block.height)
                self.top_winners = sorted(
                    [(rewards, key) for key, rewards in coin_balances.items()],
                    reverse=True,
                )[:self.num_top_block_pools]

                self.our_winners = [
                    (coin_balances[bytes(pk)],
                     bytes(pk)) if bytes(pk) in coin_balances else
                    (0, bytes(pk)) for pk in self.pool_pks
                ]
                await asyncio.sleep(5)
        except concurrent.futures._base.CancelledError as e:
            log.warn(f"Cancelled error in UI: {type(e)}: {e}")
        except Exception as e:
            log.warn(f"Exception in UI update_data {type(e)}: {e}")
            raise e

    async def await_closed(self):
        await self.update_ui_task
        await self.update_data_task
Example #6
0
class VoltronUI:
    """
	Class that manages all UI elements
	"""
    def __init__(self, buffer_queue):
        self.buffer = Buffer()
        self.modules = {}
        self.module_prompt_callback = None
        self.prompt_ident = None
        self.prompt_ident_skip = []

        key_bindings = KeyBindings()

        default_text = """
Welcome to VoltronBot!
Type ? for available commands.
Control-C or type 'quit' to exit
"""
        lexer = PygmentsLexer(VoltronOutputLexer)
        ## Main output TextArea
        self.scrolling_output = TextArea(focusable=True,
                                         text=default_text,
                                         lexer=lexer)

        self.buffer_queue = buffer_queue
        self.buffer_thread = UIBufferQueue(self, self.buffer_queue,
                                           self.scrolling_output)
        self.buffer_thread.start()
        self.prompt_queue = queue.Queue()

        ## Exit keybinds
        @key_bindings.add('c-q')
        @key_bindings.add('c-c')
        def _exit(event):
            self.buffer_queue.put('SHUTDOWN')
            self.buffer_thread.join()
            event.app.exit()

        ## TextArea for prompt
        self.prompt = TextArea(
            height=1,
            #prompt=DEFAULT_PROMPT,
            multiline=False,
            wrap_lines=True)
        self.prompt.accept_handler = self.input_recv

        ## Create status bar
        self.status_text = FormattedTextControl(text=DEFAULT_STATUS)
        self.scroll_text = FormattedTextControl(text="")

        self.status_window = Window(content=self.status_text,
                                    height=1,
                                    style="class:status-bar")
        self.scroll_window = Window(content=self.scroll_text,
                                    height=1,
                                    width=6,
                                    style="class:status-bar")
        status_split = VSplit([self.status_window, self.scroll_window])

        self.prompt_text = FormattedTextControl(text=DEFAULT_PROMPT)
        self.prompt_window = Window(content=self.prompt_text,
                                    height=1,
                                    width=len(DEFAULT_PROMPT) + 1)

        ## Create top bar
        self.main_container = HSplit([
            Window(content=FormattedTextControl(text=f"VoltronBot v{VERSION}"),
                   height=1,
                   style="class:title-bar"),
            self.scrolling_output,
            status_split,
            VSplit([self.prompt_window, self.prompt]),
        ])

        style = Style([
            ('title-bar', 'bg:ansiblue #000000'),
            ('status-bar', 'bg:ansicyan #000000'),
            ('status-bar-important', 'bg:ansired #000000'),
        ])

        self.layout = Layout(self.main_container, focused_element=self.prompt)

        ## Keybind for page up
        @key_bindings.add('pageup')
        def _scroll_up(event):
            self.layout.focus(self.scrolling_output)
            scroll_one_line_up(event)
            self.layout.focus(self.prompt)

            if not self._scrolled_to_bottom:
                self.scroll_text.text = '(more)'
            else:
                self.scroll_text.text = ''

        ## Keybind for page down
        @key_bindings.add('pagedown')
        def _scroll_down(event):
            self.layout.focus(self.scrolling_output)
            scroll_one_line_down(event)
            self.layout.focus(self.prompt)

            if not self._scrolled_to_bottom:
                self.scroll_text.text = '(more)'
            else:
                self.scroll_text.text = ''

        self._app = Application(layout=self.layout,
                                full_screen=True,
                                key_bindings=key_bindings,
                                style=style)

    @property
    def _scrolled_to_bottom(self):
        ## True if the main output is scrolled to the bottom
        if self.scrolling_output.window.render_info == None:
            return True
        return (self.scrolling_output.window.vertical_scroll +
                self.scrolling_output.window.render_info.window_height
                ) >= self.scrolling_output.window.render_info.content_height

    def build_completer(self):
        completions = {'?': {}}
        for module_name in self.modules:
            actions = {}
            for action in self.modules[module_name].available_admin_commands():
                actions[action] = None
            completions[module_name] = actions
            completions['?'][module_name] = actions

        self.prompt.completer = NestedCompleter.from_nested_dict(completions)

    def register_module(self, module):
        """
		Modules are registered through the UI so we know about admin commands

		Args:
			module (instance): The instance of the module
		"""
        if module.module_name in self.modules:
            raise Exception('Duplicate module: {}'.format(module.module_name))
        self.modules[module.module_name] = module
        self.build_completer()

    def update_status_text(self, text=None):
        """
		Update the status text on the bottom bar

		Args:
			text (string): String to show on the status bar. If None it will reset to default
		"""
        if text:
            self.status_text.text = text
            self.status_window.style = 'class:status-bar-important'
            self.scroll_window.style = 'class:status-bar-important'
        else:
            self.status_text.text = DEFAULT_STATUS
            self.status_window.style = 'class:status-bar'
            self.scroll_window.style = 'class:status-bar'
        self._app.invalidate()

    def run(self):
        self._app.run()

    def reset(self):
        self.modules = {}

    def terminate_mod_prompt(self, ident):
        """
		Cancel the prompt identified by ident

		Args:
			ident (string): Indentifier for the prompt to be cancelled
		"""
        if self.prompt_ident == ident:
            self.module_prompt_callback = None
            self.mod_prompt()

    def mod_prompt(self, prompt=None, callback=None):
        """
		Change the prompt to send input to <callback>.
		This is used in modules to receive user input

		Args:
			prompt (string): The prompt to display
			callback (func): Function to call when user input is received
		"""
        ident = uuid4().hex

        if self.module_prompt_callback and not callback:
            return

        if self.module_prompt_callback and callback:
            self.prompt_queue.put((prompt, callback, ident))
            return ident

        ## Add prompts to a queue in case a module is already waiting on a prompt
        if not callback and not self.prompt_queue.empty():
            while not self.prompt_queue.empty():
                prompt, callback, ident = self.prompt_queue.get_nowait()
                if ident in self.prompt_ident_skip:
                    self.prompt_ident_skip.remove(ident)
                    prompt, callback, ident = (None, None, None)
                else:
                    break

        self.prompt_ident = ident

        if prompt:
            prompt = prompt.strip()
            self.prompt_text.text = prompt
            self.prompt_window.width = len(prompt) + 1
        else:
            self.prompt_text.text = DEFAULT_PROMPT
            self.prompt_window.width = len(DEFAULT_PROMPT) + 1
        self.module_prompt_callback = callback

        ## Must call invalidate on app to refresh UI
        self._app.invalidate()

        ## Return the unique identifier
        return self.prompt_ident

    def input_recv(self, buff):
        """
		The default function called upon user input to the prompt
		"""
        ## If there is an active module wanting input, pass the data to
        ## the appropriate function
        if self.module_prompt_callback:
            status = self.module_prompt_callback(self.prompt.text)
            if status:
                self.module_prompt_callback = None
                self.mod_prompt(None, None)
            return

        if self.prompt.text.strip().lower() == 'quit':
            self.buffer_queue.put('SHUTDOWN')
            self.buffer_thread.join()
            get_app().exit()
            return

        ## Check for help command
        match = re.search(r'^\? ?([^ ]+)?( [^ ]+)?$', self.prompt.text)
        if match:
            module_name = match.group(1)
            command_name = match.group(2)
            if command_name:
                command_name = command_name.strip()

            self.show_help(module_name, command_name)
            return

        ## Check for a valid command
        match = re.search(r'^([^ ]+) ([^ ]+) ?(.*)$', self.prompt.text)
        if match:
            module_name = match.group(1)
            trigger = match.group(2)
            params = match.group(3)

            self._execute_admin_command(module_name, trigger, params)

    def _execute_admin_command(self, module_name, trigger, params):
        ## Execute an admin command for the appropriate module
        if not module_name in self.modules:
            pass
        elif trigger not in self.modules[module_name].available_admin_commands(
        ):
            pass
        else:
            command = self.modules[module_name].admin_command(trigger)
            command.execute(params.strip())

    def show_help(self, module=None, trigger=None):
        """
		Output help text for <module>

		Args:
			module (string): Name of the module. If none display installed modules
			trigger (string): Module command. If None display valid commands for <module>
		"""
        if module and module in self.modules.keys():
            ## Check for valid module and trigger
            if trigger and trigger in self.modules[
                    module].available_admin_commands():
                help_str = 'Help for {module} {trigger}:\n'.format(
                    module=module, trigger=trigger)
                command = self.modules[module].admin_command(trigger)
                help_str += '    ' + command.description + '\n'
                help_str += '    Usage: ' + command.usage

            else:
                ## Module specified but no trigger
                help_str = ""
                if hasattr(self.modules[module], 'module_description'):
                    help_str += self.modules[module].module_description.strip()
                    help_str += '\n\n'
                help_str += f"Commands for {module} module:\n"
                count = 0
                this_line = "    "
                for trigger in self.modules[module].available_admin_commands():
                    if count == 3:
                        help_str += f"{this_line}\n"
                        count = 0
                        this_line = "    "

                    this_line += trigger.ljust(20)
                    count += 1

                help_str += "{}\n".format(this_line)

                help_str += f"Type '? {module} <command>' for more help."
            self.buffer_queue.put(("VOLTRON", '\n' + help_str + '\n'))
        else:
            ## Show available modules
            help_str = "Available Modules:\n"
            for module_name in self.modules:
                if hasattr(self.modules[module_name], 'configurable'
                           ) and not self.modules[module_name].configurable:
                    continue
                help_str += "    {module_name}\n".format(
                    module_name=module_name)
            help_str += "Type '? <module>' for more help."
            self.buffer_queue.put(('VOLTRON', '\n' + help_str + '\n'))
Example #7
0
def pager(title, text, lexer_name="", height=10, full_screen=False):
    """Small implementation of an editor/pager for small pieces of text.

    :param title: Title of the pager
    :type  title: str

    :param text: Editable text
    :type  text: str

    :param lexer_name: If the editable text should be highlighted with
        some kind of grammar, examples are ``yaml``, ``python`` ...
    :type  lexer_name: str

    :param height: Max height of the text area
    :type  height: int

    :param full_screen: Wether or not the text area should be full screen.
    :type  full_screen: bool

    :return: Edited text, if saved, else None
    :rtype : str
    """
    from prompt_toolkit import Application
    from prompt_toolkit.enums import EditingMode
    from prompt_toolkit.buffer import Buffer
    from prompt_toolkit.layout.containers import HSplit
    from prompt_toolkit.layout.layout import Layout
    from prompt_toolkit.key_binding import KeyBindings
    from prompt_toolkit.widgets import HorizontalLine
    from .utils.general import (
         get_default_window, get_buffer_window
    )
    
    assert(type(title) == str)
    assert(type(text) == str)
    assert(type(lexer_name) == str)
    assert(type(height) == int)
    assert(type(full_screen) == bool)

    
    buffer = Buffer()
    buffer.text = text

    # Define keybindings for pager
    key_bindings = KeyBindings()
    
    # Leave pager
    @key_bindings.add('c-c')
    def exit_(event):
        event.app.exit(0)

    # Leave pager and save edited text
    @key_bindings.add('c-a')
    def save_(event):
        event.app.return_text = buffer.text
        event.app.exit(1)

    class Pager(Application):
        return_text = None

    # Generate windows with titles and text, as well as define layout of pager
    text_window = get_buffer_window(buffer, height=height, lexer_name=lexer_name, style=user._tui_text_style)

    layout= Layout(HSplit([
        get_default_window(text=title, style=user._tui_window_style),
        HorizontalLine(),
        text_window ,
        get_default_window(text="Deny [Ctrl-c] Accept [Ctrl-a]", style=user._tui_window_style),
        ])
    )
    layout.focus(text_window)

    # Get object of class Pager, set layout, edit mode as well as key bindings and run the pager
    pager = Pager(
        editing_mode=(
            EditingMode.EMACS if user._tui_edit_mode == 'emacs' else EditingMode.VI
        ), 
        layout=layout, 
        key_bindings=key_bindings, 
        full_screen=full_screen
    )
    pager.run()

    # Return the edited text, if saved
    return pager.return_text
Example #8
0
class FullNodeUI:
    """
    Full node UI instance. Displays node state, blocks, and connections. Calls parent_close_cb
    when the full node is closed. Uses the RPC client to fetch data from a full node and to display relevant
    information. The UI is updated periodically.
    """
    def __init__(self, parent_close_cb: Callable, rpc_client: RpcClient):
        self.rpc_client = rpc_client
        self.app: Optional[Application] = None
        self.data_initialized = False
        self.block = None
        self.closed: bool = False
        self.num_blocks: int = 10
        self.num_top_block_pools: int = 10
        self.top_winners: List[Tuple[uint64, bytes32]] = []
        self.our_winners: List[Tuple[uint64, bytes32]] = []
        self.prev_route: str = "home/"
        self.route: str = "home/"
        self.focused: bool = False
        self.parent_close_cb = parent_close_cb
        self.kb = self.setup_keybindings()
        self.style = Style([("error", "#ff0044")])
        self.pool_pks: List[PublicKey] = []
        key_config_filename = os.path.join(ROOT_DIR, "config", "keys.yaml")
        if os.path.isfile(key_config_filename):
            config = safe_load(open(key_config_filename, "r"))

            self.pool_pks = [
                PrivateKey.from_bytes(bytes.fromhex(ce)).get_public_key()
                for ce in config["pool_sks"]
            ]

        self.draw_initial()
        self.app = Application(
            style=self.style,
            layout=self.layout,
            full_screen=True,
            key_bindings=self.kb,
            mouse_support=True,
        )

        self.closed = False
        self.update_ui_task = asyncio.get_running_loop().create_task(
            self.update_ui())
        self.update_data_task = asyncio.get_running_loop().create_task(
            self.update_data())

    def close(self):
        # Closes this instance of the UI
        if not self.closed:
            self.closed = True
            self.route = "home/"
            if self.app:
                self.app.exit(0)

    def stop(self):
        # Closes this instance of the UI, and call parent close, which closes
        # all other instances, and shuts down the full node.
        self.close()
        self.parent_close_cb(True)

    def setup_keybindings(self) -> KeyBindings:
        kb = KeyBindings()
        kb.add("tab")(focus_next)
        kb.add("s-tab")(focus_previous)
        kb.add("down")(focus_next)
        kb.add("up")(focus_previous)
        kb.add("right")(focus_next)
        kb.add("left")(focus_previous)

        @kb.add("c-c")
        def exit_(event):
            self.close()

        return kb

    def draw_initial(self):
        search_field = SearchToolbar()
        self.empty_row = TextArea(focusable=False, height=1)

        # home/
        self.loading_msg = Label(text=f"Initializing UI....")
        self.syncing = TextArea(focusable=False, height=1)
        self.current_heads_label = TextArea(focusable=False, height=1)
        self.lca_label = TextArea(focusable=False, height=1)
        self.difficulty_label = TextArea(focusable=False, height=1)
        self.ips_label = TextArea(focusable=False, height=1)
        self.total_iters_label = TextArea(focusable=False, height=2)
        self.con_rows = []
        self.displayed_cons = set()
        self.latest_blocks: List[HeaderBlock] = []
        self.connections_msg = Label(text=f"Connections")
        self.connection_rows_vsplit = Window()
        self.add_connection_msg = Label(text=f"Add a connection ip:port")
        self.add_connection_field = TextArea(
            height=1,
            prompt=">>> ",
            style="class:input-field",
            multiline=False,
            wrap_lines=False,
            search_field=search_field,
        )
        self.add_connection_field.accept_handler = self.async_to_sync(
            self.add_connection)
        self.latest_blocks_msg = Label(text=f"Latest blocks")
        self.latest_blocks_labels = [
            Button(text="block") for _ in range(self.num_blocks)
        ]

        self.search_block_msg = Label(text=f"Search block by hash")
        self.search_block_field = TextArea(
            height=1,
            prompt=">>> ",
            style="class:input-field",
            multiline=False,
            wrap_lines=False,
            search_field=search_field,
        )
        self.search_block_field.accept_handler = self.async_to_sync(
            self.search_block)

        self.top_block_pools_msg = Label(text=f"Top block pools")
        self.top_block_pools_labels = [
            Label(text="Top block pool")
            for _ in range(self.num_top_block_pools)
        ]
        self.our_pools_msg = Label(text=f"Our pool winnings")
        self.our_pools_labels = [
            Label(text="Our winnings") for _ in range(len(self.pool_pks))
        ]

        self.close_ui_button = Button("Close UI", handler=self.close)
        self.quit_button = Button("Stop node and close UI", handler=self.stop)
        self.error_msg = Label(style="class:error", text=f"")

        # block/
        self.block_msg = Label(text=f"Block")
        self.block_label = TextArea(focusable=True,
                                    scrollbar=True,
                                    focus_on_click=True)
        self.back_button = Button(text="Back",
                                  handler=self.change_route_handler("home/"))
        self.challenge_msg = Label(text=f"Block Header")
        self.challenge = TextArea(focusable=False)

        body = HSplit([self.loading_msg], height=D(), width=D())
        self.content = Frame(title="Chia Full Node", body=body)
        self.layout = Layout(VSplit([self.content], height=D(), width=D()))

    def change_route_handler(self, route):
        def change_route():
            self.prev_route = self.route
            self.route = route
            self.focused = False
            self.error_msg.text = ""

        return change_route

    def async_to_sync(self, coroutine):
        def inner(buff=None):
            if buff is None:
                asyncio.get_running_loop().create_task(coroutine())
            else:
                asyncio.get_running_loop().create_task(coroutine(buff.text))

        return inner

    async def search_block(self, text: str):
        try:
            block = await self.rpc_client.get_block(bytes.fromhex(text))
        except ValueError:
            self.error_msg.text = "Enter a valid hex block hash"
            return
        if block is not None:
            self.change_route_handler(f"block/{text}")()
        else:
            self.error_msg.text = "Block not found"

    async def add_connection(self, text: str):
        if ":" not in text:
            self.error_msg.text = (
                "Enter a valid IP and port in the following format: 10.5.4.3:8000"
            )
            return
        else:
            ip, port = ":".join(text.split(":")[:-1]), text.split(":")[-1]
        log.info(f"Want to connect to {ip}, {port}")
        try:
            await self.rpc_client.open_connection(ip, int(port))
        except BaseException:
            # TODO: catch right exception
            self.error_msg.text = f"Failed to connect to {ip}:{port}"

    async def get_latest_blocks(
            self, heads: List[SmallHeaderBlock]) -> List[SmallHeaderBlock]:
        added_blocks: List[SmallHeaderBlock] = []
        while len(added_blocks) < self.num_blocks and len(heads) > 0:
            heads = sorted(heads, key=lambda b: b.height, reverse=True)
            max_block = heads[0]
            if max_block not in added_blocks:
                added_blocks.append(max_block)
            heads.remove(max_block)
            prev: Optional[
                SmallHeaderBlock] = await self.rpc_client.get_header(
                    max_block.prev_header_hash)
            if prev is not None:
                heads.append(prev)
        return added_blocks

    async def draw_home(self):
        connections: List[Dict] = [c for c in self.connections]
        if set([con["node_id"] for con in connections]) != self.displayed_cons:
            new_con_rows = []
            for con in connections:
                con_str = (
                    f"{NodeType(con['type']).name} {con['peer_host']} {con['peer_port']}/{con['peer_server_port']}"
                    f" {con['node_id'].hex()[:10]}...")
                con_label = Label(text=con_str)

                def disconnect(c):
                    async def inner():
                        await self.rpc_client.close_connection(c["node_id"])
                        self.layout.focus(self.quit_button)

                    return inner

                disconnect_button = Button("Disconnect",
                                           handler=self.async_to_sync(
                                               disconnect(con)))
                row = VSplit([con_label, disconnect_button])
                new_con_rows.append(row)
            self.displayed_cons = set([con["node_id"] for con in connections])
            self.con_rows = new_con_rows
            if len(self.con_rows) > 0:
                self.layout.focus(self.con_rows[0])
            else:
                self.layout.focus(self.quit_button)

        if len(self.con_rows):
            new_con_rows = HSplit(self.con_rows)
        else:
            new_con_rows = Window(width=D(), height=0)

        if self.sync_mode:
            if self.max_height >= 0:
                self.syncing.text = f"Syncing up to {self.max_height}"
            else:
                self.syncing.text = f"Syncing"
        else:
            self.syncing.text = "Not syncing"

        total_iters = self.lca_block.challenge.total_iters

        new_block_labels = []
        for i, b in enumerate(self.latest_blocks):
            self.latest_blocks_labels[i].text = (
                f"{b.height}:{b.header_hash}"
                f" {'LCA' if b.header_hash == self.lca_block.header_hash else ''}"
                f" {'TIP' if b.header_hash in [h.header_hash for h in self.tips] else ''}"
            )
            self.latest_blocks_labels[i].handler = self.change_route_handler(
                f"block/{b.header_hash}")
            new_block_labels.append(self.latest_blocks_labels[i])

        top_block_pools_labels = self.top_block_pools_labels
        if len(self.top_winners) > 0:
            new_top_block_pools_labels = []
            for i, (winnings, pk) in enumerate(self.top_winners):
                self.top_block_pools_labels[
                    i].text = f"Public key {pk.hex()}: {winnings/1000000000000} chias."
                new_top_block_pools_labels.append(
                    self.top_block_pools_labels[i])
            top_block_pools_labels = new_top_block_pools_labels

        our_pools_labels = self.our_pools_labels
        if len(self.our_winners) > 0:
            new_our_pools_labels = []
            for i, (winnings, pk) in enumerate(self.our_winners):
                self.our_pools_labels[
                    i].text = f"Public key {pk.hex()}: {winnings/(1000000000000)} chias."
                new_our_pools_labels.append(self.our_pools_labels[i])
            our_pools_labels = new_our_pools_labels

        self.lca_label.text = (
            f"Current least common ancestor {self.lca_block.header_hash}"
            f" height {self.lca_block.height}")
        self.current_heads_label.text = "Heights of tips: " + str(
            [h.height for h in self.tips])
        self.difficulty_label.text = f"Current difficulty: {self.difficulty}"

        self.ips_label.text = f"Current VDF iterations per second: {self.ips}"
        self.total_iters_label.text = f"Total iterations since genesis: {total_iters}"

        try:
            if not self.focused:
                self.layout.focus(self.close_ui_button)
                self.focused = True
        except ValueError:  # Not yet in layout
            pass
        return HSplit(
            [
                self.syncing,
                self.lca_label,
                self.current_heads_label,
                self.difficulty_label,
                self.ips_label,
                self.total_iters_label,
                Window(height=1, char="-", style="class:line"),
                self.connections_msg,
                new_con_rows,
                Window(height=1, char="-", style="class:line"),
                self.add_connection_msg,
                self.add_connection_field,
                Window(height=1, char="-", style="class:line"),
                self.latest_blocks_msg,
                *new_block_labels,
                Window(height=1, char="-", style="class:line"),
                self.search_block_msg,
                self.search_block_field,
                Window(height=1, char="-", style="class:line"),
                self.top_block_pools_msg,
                *top_block_pools_labels,
                Window(height=1, char="-", style="class:line"),
                self.our_pools_msg,
                *our_pools_labels,
                Window(height=1, char="-", style="class:line"),
                self.close_ui_button,
                self.quit_button,
                self.error_msg,
            ],
            width=D(),
            height=D(),
        )

    async def draw_block(self):
        block_hash: str = self.route.split("block/")[1]
        if self.block is None or self.block.header_hash != bytes32(
                bytes.fromhex(block_hash)):
            self.block: Optional[FullBlock] = await self.rpc_client.get_block(
                bytes32(bytes.fromhex(block_hash)))
        if self.block is not None:
            self.block_msg.text = f"Block {str(self.block.header_hash)}"
            if self.block_label.text != str(self.block):
                self.block_label.text = str(self.block)
        else:
            self.block_label.text = f"Block hash {block_hash} not found"
        try:
            if not self.focused:
                self.layout.focus(self.back_button)
                self.focused = True
        except ValueError:  # Not yet in layout
            pass
        return HSplit([self.block_msg, self.block_label, self.back_button],
                      width=D(),
                      height=D())

    async def update_ui(self):
        try:
            while not self.closed:
                if self.data_initialized:
                    if self.route.startswith("home/"):
                        self.content.body = await self.draw_home()
                    elif self.route.startswith("block/"):
                        self.content.body = await self.draw_block()

                    if self.app and not self.app.invalidated:
                        self.app.invalidate()
                await asyncio.sleep(0.5)
        except Exception as e:
            log.error(f"Exception in UI update_ui {type(e)}: {e}")
            raise e

    async def update_data(self):
        self.data_initialized = False
        counter = 0
        try:
            while not self.closed:
                try:
                    blockchain_state = await self.rpc_client.get_blockchain_state(
                    )
                    self.lca_block = blockchain_state["lca"]
                    self.tips = blockchain_state["tips"]
                    self.difficulty = blockchain_state["difficulty"]
                    self.ips = blockchain_state["ips"]
                    self.sync_mode = blockchain_state["sync_mode"]
                    self.connections = await self.rpc_client.get_connections()
                    if self.sync_mode:
                        max_block = await self.rpc_client.get_heaviest_block_seen(
                        )
                        self.max_height = max_block.height

                    self.latest_blocks = await self.get_latest_blocks(self.tips
                                                                      )

                    self.data_initialized = True
                    if counter % 20 == 0:
                        # Only request balances periodically, since it's an expensive operation
                        coin_balances: Dict[
                            bytes,
                            uint64] = await self.rpc_client.get_pool_balances(
                            )
                        self.top_winners = sorted(
                            [(rewards, key)
                             for key, rewards in coin_balances.items()],
                            reverse=True,
                        )[:self.num_top_block_pools]

                        self.our_winners = [
                            (coin_balances[bytes(pk)],
                             bytes(pk)) if bytes(pk) in coin_balances else
                            (0, bytes(pk)) for pk in self.pool_pks
                        ]

                    counter += 1
                    await asyncio.sleep(5)
                except (
                        aiohttp.client_exceptions.ClientConnectorError,
                        aiohttp.client_exceptions.ServerConnectionError,
                ) as e:
                    log.warning(
                        f"Could not connect to full node. Is it running? {e}")
                    await asyncio.sleep(5)
        except Exception as e:
            log.error(f"Exception in UI update_data {type(e)}: {e}")
            raise e

    async def await_closed(self):
        await self.update_ui_task
        await self.update_data_task
Example #9
0
class QuerySourceView(object):
    """Bind input, visual elements to query_view_model logic. """
    def __init__(self, query_view_model):

        self.view_model = query_view_model
        self.shared_state = self.view_model.shared_state

        # layout components:
        self.query_header = Window(
            FormattedTextControl(query_title_bar_text(self.shared_state)),
            height=1,
            style="reverse",
        )

        self.query_window = Window(
            BufferControl(self.view_model.query_buffer, ),
            height=1,
        )
        results_window = Window(
            self.view_model.results_textcontrol,
            height=Dimension(**RESULTS_DIMENSION_DICT),
        )

        preview_header = Window(self.view_model.preview_header,
                                height=1,
                                style="reverse")

        preview_window = Window(
            self.view_model.preview_textcontrol,
            wrap_lines=True,
            height=Dimension(**PREVIEW_DIMENSION_DICT),
        )
        status_window = Window(self.view_model.status_textcontrol,
                               height=1,
                               style="reverse")

        # GENERATE LAYOUT
        self.layout = Layout(
            HSplit([
                self.query_header,
                self.query_window,
                results_window,
                preview_header,
                preview_window,
                status_window,
            ]), )

        # SEARCH PAGE KEYBINDINGS
        self.kb = KeyBindings()

        # select result:
        @self.kb.add("up")
        def _(event):
            self.view_model.index -= 1

        @self.kb.add("down")
        def _(event):
            self.view_model.index += 1

        @self.kb.add("escape")
        def _(event):
            self.view_model.query_str = ""

        @self.kb.add("enter")
        def _(event):
            """generate records from source."""
            try:
                self.view_model.update_results()
            except Exception:
                self.view_model.status_textcontrol.text = "(no results available)"

        @self.kb.add("s-tab")
        def _(event):
            """add all records to collection."""

            N = len(self.view_model.results)
            coll = self.shared_state["active_collection"]
            self.view_model.status_textcontrol.text = (
                f"adding {N} records to {coll.name}...")
            count = 0
            for record in self.view_model.results:
                try:
                    coll.add_document(record_id=record["record_id"])
                    count += 1
                except Exception:
                    pass
            self.view_model.status_textcontrol.text = (
                f"added {count} records to {coll.name}.")

        @self.kb.add("s-right")
        def _(event):
            """add specific record to collection."""

            record_id = self.view_model.results[
                self.view_model.index]["record_id"]
            coll = self.shared_state["active_collection"]
            self.view_model.status_textcontrol.text = (
                f"adding {record_id} records to {coll.name}...")
            coll.add_document(record_id=record_id)
            self.view_model.status_textcontrol.text = (
                f"added {record_id} to {coll.name}")

    def refresh_view(self):
        """Code when screen is changed."""
        # self.view_model.query_str = ""
        self.query_header.content.text = query_title_bar_text(
            self.shared_state)
        # self.view_model.update_results()
        self.layout.focus(self.query_window)
Example #10
0
class FuzzyPrompt(BaseListPrompt):
    """Create a prompt that lists choices while also allowing fuzzy search like fzf.

    A wrapper class around :class:`~prompt_toolkit.application.Application`.

    Fuzzy search using :func:`pfzy.match.fuzzy_match` function.

    Override the default keybindings for up/down as j/k cannot be bind even if `editing_mode` is vim
    due to the input buffer.

    Args:
        message: The question to ask the user.
            Refer to :ref:`pages/dynamic:message` documentation for more details.
        choices: List of choices to display and select.
            Refer to :ref:`pages/dynamic:choices` documentation for more details.
        style: An :class:`InquirerPyStyle` instance.
            Refer to :ref:`Style <pages/style:Alternate Syntax>` documentation for more details.
        vi_mode: Use vim keybinding for the prompt.
            Refer to :ref:`pages/kb:Keybindings` documentation for more details.
        default: Set the default value in the search buffer.
            Different than other list type prompts, the `default` parameter tries to replicate what fzf does and
            add the value in `default` to search buffer so it starts searching immediatelly.
            Refer to :ref:`pages/dynamic:default` documentation for more details.
        qmark: Question mark symbol. Custom symbol that will be displayed infront of the question before its answered.
        amark: Answer mark symbol. Custom symbol that will be displayed infront of the question after its answered.
        pointer: Pointer symbol. Customer symbol that will be used to indicate the current choice selection.
        instruction: Short instruction to display next to the question.
        long_instruction: Long instructions to display at the bottom of the prompt.
        validate: Add validation to user input.
            The main use case for this prompt would be when `multiselect` is True, you can enforce a min/max selection.
            Refer to :ref:`pages/validator:Validator` documentation for more details.
        invalid_message: Error message to display when user input is invalid.
            Refer to :ref:`pages/validator:Validator` documentation for more details.
        transformer: A function which performs additional transformation on the value that gets printed to the terminal.
            Different than `filter` parameter, this is only visual effect and won’t affect the actual value returned by :meth:`~InquirerPy.base.simple.BaseSimplePrompt.execute`.
            Refer to :ref:`pages/dynamic:transformer` documentation for more details.
        filter: A function which performs additional transformation on the result.
            This affects the actual value returned by :meth:`~InquirerPy.base.simple.BaseSimplePrompt.execute`.
            Refer to :ref:`pages/dynamic:filter` documentation for more details.
        height: Preferred height of the prompt.
            Refer to :ref:`pages/height:Height` documentation for more details.
        max_height: Max height of the prompt.
            Refer to :ref:`pages/height:Height` documentation for more details.
        multiselect: Enable multi-selection on choices.
            You can use `validate` parameter to control min/max selections.
            Setting to True will also change the result from a single value to a list of values.
        prompt: Input prompt symbol. Custom symbol to display infront of the input buffer to indicate for input.
        border: Create border around the choice window.
        info: Display choice information similar to fzf --info=inline next to the prompt.
        match_exact: Use exact sub-string match instead of using fzy fuzzy match algorithm.
        exact_symbol: Custom symbol to display in the info section when `info=True`.
        marker: Marker Symbol. Custom symbol to indicate if a choice is selected.
            This will take effects when `multiselect` is True.
        marker_pl: Marker place holder when the choice is not selected.
            This is empty space by default.
        keybindings: Customise the builtin keybindings.
            Refer to :ref:`pages/kb:Keybindings` for more details.
        cycle: Return to top item if hit bottom during navigation or vice versa.
        wrap_lines: Soft wrap question lines when question exceeds the terminal width.
        raise_keyboard_interrupt: Raise the :class:`KeyboardInterrupt` exception when `ctrl-c` is pressed. If false, the result
            will be `None` and the question is skiped.
        mandatory: Indicate if the prompt is mandatory. If True, then the question cannot be skipped.
        mandatory_message: Error message to show when user attempts to skip mandatory prompt.
        session_result: Used internally for :ref:`index:Classic Syntax (PyInquirer)`.

    Examples:
        >>> from InquirerPy import inquirer
        >>> result = inquirer.fuzzy(message="Select one:", choices=[1, 2, 3]).execute()
        >>> print(result)
        1
    """

    def __init__(
        self,
        message: InquirerPyMessage,
        choices: InquirerPyListChoices,
        default: InquirerPyDefault = "",
        pointer: str = INQUIRERPY_POINTER_SEQUENCE,
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        amark: str = "?",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        instruction: str = "",
        long_instruction: str = "",
        multiselect: bool = False,
        prompt: str = INQUIRERPY_POINTER_SEQUENCE,
        marker: str = INQUIRERPY_POINTER_SEQUENCE,
        marker_pl: str = " ",
        border: bool = False,
        info: bool = True,
        match_exact: bool = False,
        exact_symbol: str = " E",
        height: Union[str, int] = None,
        max_height: Union[str, int] = None,
        validate: InquirerPyValidate = None,
        invalid_message: str = "Invalid input",
        keybindings: InquirerPyKeybindings = None,
        cycle: bool = True,
        wrap_lines: bool = True,
        raise_keyboard_interrupt: bool = True,
        mandatory: bool = True,
        mandatory_message: str = "Mandatory prompt",
        session_result: InquirerPySessionResult = None,
    ) -> None:
        if not keybindings:
            keybindings = {}
        self._prompt = prompt
        self._info = info
        self._task = None
        self._rendered = False
        self._exact_symbol = exact_symbol

        keybindings = {
            "up": [{"key": "up"}, {"key": "c-p"}],
            "down": [{"key": "down"}, {"key": "c-n"}],
            "toggle": [],
            "toggle-exact": [],
            **keybindings,
        }
        super().__init__(
            message=message,
            style=style,
            border=border,
            vi_mode=vi_mode,
            qmark=qmark,
            amark=amark,
            transformer=transformer,
            filter=filter,
            validate=validate,
            invalid_message=invalid_message,
            multiselect=multiselect,
            instruction=instruction,
            long_instruction=long_instruction,
            keybindings=keybindings,
            cycle=cycle,
            wrap_lines=wrap_lines,
            raise_keyboard_interrupt=raise_keyboard_interrupt,
            mandatory=mandatory,
            mandatory_message=mandatory_message,
            session_result=session_result,
        )
        self.kb_func_lookup = {"toggle-exact": [{"func": self._toggle_exact}]}
        self._default = (
            default
            if not isinstance(default, Callable)
            else cast(Callable, default)(self._result)
        )
        self._height_offset += 1  # search input
        self._dimmension_height, self._dimmension_max_height = calculate_height(
            height, max_height, height_offset=self.height_offset
        )

        self._content_control: InquirerPyFuzzyControl = InquirerPyFuzzyControl(
            choices=choices,
            pointer=pointer,
            marker=marker,
            current_text=self._get_current_text,
            max_lines=self._dimmension_max_height,
            session_result=session_result,
            multiselect=multiselect,
            marker_pl=marker_pl,
            match_exact=match_exact,
        )

        self._buffer = Buffer(on_text_changed=self._on_text_changed)
        input_window = Window(
            height=LayoutDimension.exact(1),
            content=BufferControl(
                self._buffer,
                [
                    AfterInput(self._generate_after_input),
                    BeforeInput(self._generate_before_input),
                ],
                lexer=SimpleLexer("class:input"),
            ),
        )

        choice_height_dimmension = lambda: Dimension(
            max=self._dimmension_max_height,
            preferred=self._dimmension_height,
            min=self.content_control._height if self.content_control._height > 0 else 1,
        )
        self.choice_window = Window(
            content=self.content_control,
            height=choice_height_dimmension,
            dont_extend_height=True,
        )

        main_content_window = HSplit([input_window, self.choice_window])
        if self._border:
            main_content_window = Frame(main_content_window)
        self._layout = Layout(
            FloatContainer(
                content=HSplit(
                    [
                        MessageWindow(
                            message=self._get_prompt_message,
                            filter=True,
                            wrap_lines=self._wrap_lines,
                            show_cursor=True,
                        ),
                        ConditionalContainer(
                            main_content_window,
                            filter=~IsDone(),
                        ),
                        ConditionalContainer(
                            Window(content=DummyControl()),
                            filter=~IsDone() & self._is_displaying_long_instruction,
                        ),
                        InstructionWindow(
                            message=self._long_instruction,
                            filter=~IsDone() & self._is_displaying_long_instruction,
                            wrap_lines=self._wrap_lines,
                        ),
                    ],
                ),
                floats=[
                    ValidationFloat(
                        invalid_message=self._get_error_message,
                        filter=self._is_invalid & ~IsDone(),
                        wrap_lines=self._wrap_lines,
                        left=0,
                        bottom=self._validation_window_bottom_offset,
                    ),
                ],
            )
        )
        self._layout.focus(input_window)

        self._application = Application(
            layout=self._layout,
            style=self._style,
            key_bindings=self._kb,
            editing_mode=self._editing_mode,
            after_render=self._after_render,
        )

    def _toggle_exact(self, _, value: bool = None) -> None:
        """Toggle matching algorithm.

        Switch between fzy fuzzy match or sub-string exact match.

        Args:
            value: Specify the value to toggle.
        """
        if value is not None:
            self.content_control._scorer = fzy_scorer if not value else substr_scorer
        else:
            self.content_control._scorer = (
                fzy_scorer
                if self.content_control._scorer == substr_scorer
                else substr_scorer
            )

    def _on_rendered(self, _) -> None:
        """Render callable choices and set the buffer default text.

        Setting buffer default text has to be after application is rendered and choice are loaded,
        because `self._filter_choices` will use the event loop from `Application`.
        """
        if self._default:
            default_text = str(self._default)
            self._buffer.text = default_text
            self._buffer.cursor_position = len(default_text)

    def _handle_toggle_all(self, _, value: bool = None) -> None:
        """Toggle all choice `enabled` status.

        Args:
            value: Specify the value to toggle.
        """
        if not self._multiselect:
            return
        for choice in self.content_control._filtered_choices:
            raw_choice = self.content_control.choices[choice["index"]]
            if isinstance(raw_choice["value"], Separator):
                continue
            raw_choice["enabled"] = value if value else not raw_choice["enabled"]

    def _generate_after_input(self) -> List[Tuple[str, str]]:
        """Virtual text displayed after the user input."""
        display_message = []
        if self._info:
            display_message.append(("", "  "))
            display_message.append(
                (
                    "class:fuzzy_info",
                    f"{self.content_control.choice_count}/{len(self.content_control.choices)}",
                )
            )
            if self._multiselect:
                display_message.append(
                    ("class:fuzzy_info", f" ({len(self.selected_choices)})")
                )
            if self.content_control._scorer == substr_scorer:
                display_message.append(("class:fuzzy_info", self._exact_symbol))
        return display_message

    def _generate_before_input(self) -> List[Tuple[str, str]]:
        """Display prompt symbol as virtual text before user input."""
        display_message = []
        display_message.append(("class:fuzzy_prompt", "%s " % self._prompt))
        return display_message

    def _filter_callback(self, task):
        """Redraw `self._application` when the filter task is finished."""
        if task.cancelled():
            return
        self.content_control._filtered_choices = task.result()
        self._application.invalidate()

    def _calculate_wait_time(self) -> float:
        """Calculate wait time to smoother the application on big data set.

        Using digit of the choices lengeth to get wait time.
        For digit greater than 6, using formula 2^(digit - 5) * 0.3 to increase the wait_time.

        Returns:
            Desired wait time before running the filter.
        """
        wait_table = {
            2: 0.05,
            3: 0.1,
            4: 0.2,
            5: 0.3,
        }
        digit = 1
        if len(self.content_control.choices) > 0:
            digit = int(math.log10(len(self.content_control.choices))) + 1

        if digit < 2:
            return 0.0
        if digit in wait_table:
            return wait_table[digit]
        return wait_table[5] * (2 ** (digit - 5))

    def _on_text_changed(self, _) -> None:
        """Handle buffer text change event.

        1. Check if there is current task running.
        2. Cancel if already has task, increase wait_time
        3. Create a filtered_choice task in asyncio event loop
        4. Add callback

        1. Run a new filter on all choices.
        2. Re-calculate current selected_choice_index
            if it exceeds the total filtered_choice.
        3. Avoid selected_choice_index less than zero,
            this fix the issue of cursor lose when:
            choice -> empty choice -> choice

        Don't need to create or check asyncio event loop, `prompt_toolkit`
        application already has a event loop running.
        """
        if self._invalid:
            self._invalid = False
        wait_time = self._calculate_wait_time()
        if self._task and not self._task.done():
            self._task.cancel()
        self._task = asyncio.create_task(
            self.content_control._filter_choices(wait_time)
        )
        self._task.add_done_callback(self._filter_callback)

    def _handle_toggle_choice(self, _) -> None:
        """Handle tab event, alter the `selected` state of the choice."""
        if not self._multiselect:
            return
        current_selected_index = self.content_control.selection["index"]
        self.content_control.choices[current_selected_index][
            "enabled"
        ] = not self.content_control.choices[current_selected_index]["enabled"]

    def _handle_enter(self, event) -> None:
        """Handle enter event.

        Validate the result first.

        In multiselect scenario, if no TAB is entered, then capture the current
        highlighted choice and return the value in a list.
        Otherwise, return all TAB choices as a list.

        In normal scenario, reutrn the current highlighted choice.

        If current UI contains no choice due to filter, return None.
        """
        try:
            fake_document = FakeDocument(self.result_value)
            self._validator.validate(fake_document)  # type: ignore
            if self._multiselect:
                self.status["answered"] = True
                if not self.selected_choices:
                    self.status["result"] = [self.content_control.selection["name"]]
                    event.app.exit(result=[self.content_control.selection["value"]])
                else:
                    self.status["result"] = self.result_name
                    event.app.exit(result=self.result_value)
            else:
                self.status["answered"] = True
                self.status["result"] = self.content_control.selection["name"]
                event.app.exit(result=self.content_control.selection["value"])
        except ValidationError as e:
            self._set_error(str(e))
        except IndexError:
            self.status["answered"] = True
            self.status["result"] = None if not self._multiselect else []
            event.app.exit(result=None if not self._multiselect else [])

    @property
    def content_control(self) -> InquirerPyFuzzyControl:
        """InquirerPyFuzzyControl: Override for type-hinting."""
        return cast(InquirerPyFuzzyControl, super().content_control)

    @content_control.setter
    def content_control(self, value: InquirerPyFuzzyControl) -> None:
        self._content_control = value

    def _get_current_text(self) -> str:
        """Get current input buffer text."""
        return self._buffer.text
Example #11
0
def text_area(
        title: str,
        text: str,
        lexer_name: str = "",
        height: int = 10,
        full_screen: bool = False) -> str:
    """
    Small implementation of an editor/pager for small pieces of text.

    :param title: Title of the text_area
    :type  title: str
    :param text: Editable text
    :type  text: str
    :param lexer_name: If the editable text should be highlighted with
        some kind of grammar, examples are ``yaml``, ``python`` ...
    :type  lexer_name: str
    :param height: Max height of the text area
    :type  height: int
    :param full_screen: Wether or not the text area should be full screen.
    :type  full_screen: bool
    """
    from prompt_toolkit import Application
    from prompt_toolkit.enums import EditingMode
    from prompt_toolkit.buffer import Buffer
    from prompt_toolkit.layout.containers import HSplit, Window, WindowAlign
    from prompt_toolkit.layout.controls import (
        BufferControl, FormattedTextControl
    )
    from prompt_toolkit.layout.layout import Layout
    from prompt_toolkit.utils import Event
    from prompt_toolkit.layout import Dimension
    from prompt_toolkit.key_binding import KeyBindings
    from prompt_toolkit.lexers import PygmentsLexer
    from pygments.lexers import find_lexer_class_by_name

    kb = KeyBindings()
    buffer1 = Buffer()
    buffer1.text = text

    @kb.add('c-q')  # type: ignore
    def exit_(event: Event) -> None:
        event.app.exit(0)

    @kb.add('c-s')  # type: ignore
    def save_(event: Event) -> None:
        event.app.return_text = buffer1.text

    class App(Application):  # type: ignore
        # TODO: add stubs to be able to remove type ignore above
        return_text = ""  # type: str

    text_height = Dimension(min=0, max=height) if height is not None else None

    pygment_lexer = find_lexer_class_by_name(lexer_name)
    lexer = PygmentsLexer(pygment_lexer)
    text_window = Window(
        height=text_height,
        style='bg:black fg:ansiwhite',
        content=BufferControl(buffer=buffer1, lexer=lexer))

    root_container = HSplit([
        Window(
            align=WindowAlign.LEFT,
            style='bg:ansiwhite',
            height=1,
            content=FormattedTextControl(
                text=[('fg:ansiblack bg:ansiwhite', title)]
            ),
            always_hide_cursor=True
        ),

        text_window,

        Window(
            height=1,
            width=None,
            align=WindowAlign.LEFT,
            style='bg:ansiwhite',
            content=FormattedTextControl(
                text=[(
                    'fg:ansiblack bg:ansiwhite',
                    "Quit [Ctrl-q]  Save [Ctrl-s]"
                )]
            )
        ),
    ])

    layout = Layout(root_container)

    layout.focus(text_window)

    app = App(
        editing_mode=EditingMode.EMACS,
        layout=layout,
        key_bindings=kb,
        full_screen=full_screen)
    app.run()
    return app.return_text
Example #12
0
class UICard(metaclass=Singleton):
    uicard_data = None
    uicard_state = None
    uicard_callbacks = None

    frontal_buffer = None
    hidden_buffer = None
    tag_buffer = None
    buttons = []
    edit_buttons = []

    kb = None
    layout = None

    def __init__(self, uicard_data, uicard_state, uicard_callbacks):
        self.uicard_data = uicard_data
        self.uicard_state = uicard_state
        self.uicard_callbacks = uicard_callbacks
        self.buttons_box = None

        self.build_ui()
        self.bind_keyboard()




    def build_ui(self):

        self.frontal_buffer = Buffer()
        self.frontal_buffer.wrap_lines = True
        self.frontal_buffer.dont_extend_width = True

        self.hidden_buffer = Buffer()
        self.hidden_buffer.wrap_lines = True
        self.hidden_buffer.dont_extend_width = True

        self.tag_buffer = Buffer()
        self.tag_buffer.wrap_lines = True
        self.tag_buffer.dont_extend_width = True
        self.tag_buffer.dont_extend_height = True

        empty_buttons = [
            Button('Empy', handler=quit_app),
            Button('Empy', handler=quit_app),
        ]

        self.toolbar = VSplit(empty_buttons, height=1)

        left_panel = Window(content=BufferControl(buffer=self.frontal_buffer), wrap_lines=True)
        right_panel = Window(content=BufferControl(buffer=self.hidden_buffer), wrap_lines=True)
        self.bottom_panel = Box(self.toolbar, height=4)
        tag_panel = VSplit([
        Window(content=FormattedTextControl(text='TAG: '), width=5),
        Window(content=BufferControl(buffer=self.tag_buffer))
        ], height=1)




        vertical_container = VSplit([
        Window(width=2, char='| '),
        left_panel,
        Window(width=3, char=' | '),
        right_panel,
        Window(width=2, char=' |'),
        ])

        title_panel = VSplit([
        Window(width=2, char='| '),
        Window(content=FormattedTextControl(text='Frontal Face')),
        Window(width=3, char=' | '),
        Window(content=FormattedTextControl(text='Hidden Face')),
        Window(width=2, char=' |')
        ], height=1)

        footer_panel = VSplit([
        Window(width=2, char='| '),
        Window(content=FormattedTextControl(text='Status bar')),
        Window(width=3, char=' | '),
        tag_panel,
        Window(width=2, char=' |'),
        ], height=1)

        root_container = HSplit([
        Window(height=1, char='='),
        title_panel,
        Window(height=1, char='-'),
        vertical_container,
        Window(height=1, char='-'),
        footer_panel,
        Window(height=1, char='-'),
        self.bottom_panel
        ])

        self.layout = Layout(root_container)
        self.show_toolbar('pratice')


    def show_toolbar(self, id):
        toolbar = get_toolbar(self.uicard_state.toolbars, id)
        self.toolbar.children = toolbar.toolbar_panel.children
        self.layout.focus(toolbar.prompt_buttons[0])



    def change_mode(self, mode):
        self.uicard_state.mode = mode
        if get_toolbar(self.uicard_state.toolbars, mode) is not None:
            self.show_toolbar(mode)





    def bind_keyboard(self):


        kb = KeyBindings()
        self.kb = kb
        kb.add("tab")(focus_next)
        kb.add("s-tab")(focus_previous)
        kb.add('s-left')(focus_previous)
        kb.add('s-right')(focus_next)
        kb.add('c-q')(quit_app)

        for shortcut in self.uicard_state.shortcuts:
            kb.add(shortcut.shortcut)(shortcut.callback)


    def edit_mode(self, event):
        self.uicard_state.mode = 'edit'
        self.custom_buttons_vsplit.children = self.edit_buttons_vsplit.children
        self.layout.focus(self.edit_buttons[0])



    def fire(self):
        if self.uicard_state.frontal_editable == False:
            self.frontal_buffer.text = self.uicard_data.frontal
        if self.uicard_state.hidden_editable == False:
            if self.uicard_state.show_hidden:
                self.hidden_buffer.text = self.uicard_data.hidden
            else:
                self.hidden_buffer.text = ''
        if self.uicard_state.tag_editable == False:
            self.tag_buffer.text = self.uicard_data.tag



    def create_app(self):
        app = Application(key_bindings=self.kb,layout=self.layout, full_screen=True)
        app.before_render = self

        return app
Example #13
0
def start_app(store: RetroStore, connection_string: str = ''):
    kb = KeyBindings()

    @kb.add('c-q')
    def exit_(event):
        event.app.exit()

    @kb.add('c-r')
    def refresh_(event):
        refresh()

    def refresh():
        items = store.list()

        texts = {
            Category.GOOD: StringIO(),
            Category.NEUTRAL: StringIO(),
            Category.BAD: StringIO(),
        }
        for item in items:
            texts[item.category].write(f'{item.key}. {item.text}\n')

        good_buffer.text = texts[Category.GOOD].getvalue()
        neutral_buffer.text = texts[Category.NEUTRAL].getvalue()
        bad_buffer.text = texts[Category.BAD].getvalue()

    @kb.add('c-m')
    def enter_(event):
        text = input_buffer.text

        if text.startswith('+'):
            input_buffer.reset()
            store.add_item(text[1:].strip(), Category.GOOD)
        elif text.startswith('.'):
            input_buffer.reset()
            store.add_item(text[1:].strip(), Category.NEUTRAL)
        elif text.startswith('-'):
            input_buffer.reset()
            store.add_item(text[1:].strip(), Category.BAD)

        elif text.startswith('mv '):
            cmd, key, column = text.split()

            categories = {
                '+': Category.GOOD,
                '.': Category.NEUTRAL,
                '-': Category.BAD,
            }

            input_buffer.reset()
            store.move_item(int(key), categories[column])

        elif text.startswith('rm '):
            cmd, key = text.split()

            input_buffer.reset()
            store.remove(int(key))

        refresh()

    @kb.add('c-p')
    def ping_(event):
        start = time()
        store.list(Category.GOOD)
        app.print_text(f'latency: {time() - start:.3f}')

    good_buffer = Buffer()
    neutral_buffer = Buffer()
    bad_buffer = Buffer()

    input_buffer = Buffer()
    input = Window(content=BufferControl(buffer=input_buffer), height=1)

    root_container = HSplit([
        VSplit([
            HSplit([
                Window(content=FormattedTextControl(text=':)'),
                       height=1,
                       align=WindowAlign.CENTER),
                Window(height=1, char='-'),
                Window(content=BufferControl(buffer=good_buffer)),
            ],
                   style="fg:white bold bg:ansigreen"),
            Window(width=2, char='|'),
            HSplit([
                Window(content=FormattedTextControl(text=':|'),
                       height=1,
                       align=WindowAlign.CENTER),
                Window(height=1, char='-'),
                Window(content=BufferControl(buffer=neutral_buffer)),
            ],
                   style="fg:white bold bg:ansiyellow"),
            Window(width=2, char='|'),
            HSplit([
                Window(content=FormattedTextControl(text=':('),
                       height=1,
                       align=WindowAlign.CENTER),
                Window(height=1, char='-'),
                Window(content=BufferControl(buffer=bad_buffer)),
            ],
                   style="fg:white bold bg:ansired"),
        ]),
        Window(
            content=FormattedTextControl(text=f'Invite: {connection_string}'),
            height=1,
            align=WindowAlign.CENTER), input
    ],
                            style='bg:grey')

    layout = Layout(root_container)
    layout.focus(input)

    app = Application(layout=layout, key_bindings=kb, full_screen=True)

    async def active_refresh():
        while True:
            refresh()
            await asyncio.sleep(2)

    app.create_background_task(active_refresh())

    app.run()
Example #14
0
class CollectionsView(object):
    """View screen with selectable results, preview."""
    def __init__(
        self,
        state,
    ):
        self.state = state
        self.kb = KeyBindings()

        # layout components
        self.header_bar = FormattedTextControl(focusable=False, )
        self.input_buffer = Buffer(multiline=False, )
        self.input_buffer.on_text_changed += self.update_results
        self.results_control = SelectableList(text="")
        self.preview_bar = FormattedTextControl(focusable=False, )
        self.preview_buffer = BufferControl(
            input_processors=[TabsProcessor(tabstop=4, char1="", char2="")],
            focusable=False,
        )
        self.status_bar = FormattedTextControl()
        self.layout = Layout(
            HSplit([
                Window(self.header_bar, height=1, style="reverse"),
                Window(
                    BufferControl(self.input_buffer),
                    height=1,
                ),
                Window(self.results_control,
                       height=Dimension(**RESULTS_DIMENSION_DICT)),
                Window(self.preview_bar, height=1, style="reverse"),
                Window(
                    self.preview_buffer,
                    wrap_lines=True,
                    height=Dimension(**PREVIEW_DIMENSION_DICT),
                ),
                Window(self.status_bar, height=1, style="reverse"),
            ]), )
        self.reset_view()

        @self.kb.add("up")
        def _(event):
            self.results_control.index -= 1
            self.update_preview()

        @self.kb.add("down")
        def _(event):
            self.results_control.index += 1
            self.update_preview()

        @self.kb.add("enter")
        def _(event):
            self.select_collection()
            self.reset_view()

    @property
    def input_str(self):
        return self.input_buffer.text

    @input_str.setter
    def input_str(self, text):
        self.input_buffer.text = text

    def select_collection(self, collection=None):

        if collection is None:
            collection = self.state["collections"][
                self.results_control.selected_result]
        self.state["active_collection"] = collection
        self.input_str = collection.collection_id
        self.input_buffer.cursor_position = len(self.input_str)

    def update_results(self, unused_arg=""):
        """Update self.results."""

        results = sorted(Collection.registered_collections())
        # results may be empty
        if results:
            result_scores = {
                coll: fuzz.ratio(coll, self.input_str)
                for coll in results
            }
            max_score = max(result_scores.values())

            for idx, result in enumerate(results):
                if result_scores[result] == max_score:
                    self.results_control.text = results
                    self.results_control.index = idx
                    break
        else:
            self.results_control.index = -1
            self.results_control.text = ["(no registered collections)"]
        self.update_preview()

    def update_header_bar(self, text=None):
        """Update the header text."""
        if text is None:
            text = f"Active collection: {self.state['active_collection'].collection_id}"
        self.header_bar.text = text

    def update_status_bar(self, text=None):
        """Update the status bar text."""
        coll = self.state["active_collection"]
        df = coll.records_db.df
        _doc_count = df.document_type.value_counts().reset_index(name="counts")
        doc_count_str = ", ".join(
            [f"{row[1]} {row[0].__name__}" for row in _doc_count.values])
        if text is None:
            text = (f"{coll.records_db.df.shape[0]} docs in "
                    f"{coll.collection_id}: "
                    f"{doc_count_str}")
        self.status_bar.text = text

    def update_preview_bar(self, text=None):
        """Update the preview bar text."""
        coll = self.state["active_collection"]
        if text is None:
            text = "Collection preview"
        self.preview_bar.text = text

    def update_preview(self):
        """Update preview window text."""
        try:
            coll = self.state["collections"][
                self.results_control.selected_result]
            preview = coll.preview()
        except KeyError:
            preview = f"(no preview available)"
        self.preview_buffer.buffer.text = preview

    def reset_view(self):
        """Update all values from shared state dict."""
        self.update_header_bar()
        self.update_preview_bar()
        self.update_preview()
        self.update_status_bar()
        self.update_results()
        self.layout.focus(self.input_buffer)