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'
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
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
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
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
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'))
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
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
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)
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
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
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
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()
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)