Пример #1
0
class PyTicker(object):
    def __init__(self, pyticker_db: PyTickerDBOperations):
        self._application = None
        self._pyticker_layout = PyTickerLayout(pyticker_db)
        self._watchlist_view = WatchListView()
        self._positions_view = PositionsView()
        self._pyticker_db = pyticker_db

    def init_application(self):
        layout = self._pyticker_layout.get_layout()
        self._application = Application(layout=layout,
                                        full_screen=True,
                                        key_bindings=bindings)

    def _invalidate(self):
        watchlist_stock_symbols, position_stock_symbols = self._pyticker_db.get_stock_symobls_to_fetch_quotes(
        )
        yahoo_client = YahooHttpClient(watchlist_stock_symbols,
                                       position_stock_symbols,
                                       self._pyticker_db)
        watchlist_text, position_text = yahoo_client.get_stock_quotes()
        WATCHLIST_STOCKS_TEXT.text = watchlist_text
        POSITION_STOCKS_TEXT.text = position_text
        self._application.invalidate()

    def run(self):
        watchlist_stock_symbols, position_stock_symbols = self._pyticker_db.get_stock_symobls_to_fetch_quotes(
        )
        yahoo_client = YahooHttpClient(watchlist_stock_symbols,
                                       position_stock_symbols,
                                       self._pyticker_db)
        watchlist_text, position_text = yahoo_client.get_stock_quotes()
        WATCHLIST_STOCKS_TEXT.text = watchlist_text
        POSITION_STOCKS_TEXT.text = position_text
        threading.Thread(target=lambda: every(1, self._invalidate),
                         daemon=True).start()
        self._application.run()
Пример #2
0
class PtDebugCli:
    """ Command line interface using prompt_toolkit. """
    def __init__(self, debugger):
        self._filename = None
        self.sources = {}
        self.debugger = debugger
        self.debugger.events.on_stop += self.on_stop
        self.current_address_margin = CurrentAddressMargin()
        kb = KeyBindings()
        self.locals_processor = DisplayVariablesProcessor()

        self.source_buffer = Buffer(multiline=True)
        self.bar_buffer = Buffer(multiline=True)
        self.register_buffer = Buffer(multiline=True)
        self.logs_buffer = Buffer(multiline=True)

        @kb.add(Keys.F10, eager=True)
        def quit_(event):
            event.app.exit()

        @kb.add(Keys.F8)
        def clear_breakpoint_(event):
            if self.has_source():
                filename, row = self.get_current_location()
                self.debugger.clear_breakpoint(filename, row)

        @kb.add(Keys.F7)
        def set_breakpoint_(event):
            if self.has_source():
                filename, row = self.get_current_location()
                self.debugger.set_breakpoint(filename, row)

        @kb.add(Keys.F6)
        def step_(event):
            self.debugger.step()

        @kb.add(Keys.F5)
        def run_(event):
            self.debugger.run()

        @kb.add(Keys.F4)
        def stop_(event):
            self.debugger.stop()

        @kb.add(Keys.PageUp)
        def scroll_up_(event):
            self.source_buffer.cursor_up(count=15)

        @kb.add(Keys.PageDown)
        def scroll_down_(event):
            self.source_buffer.cursor_down(count=15)

        src_lexer = PygmentsLexer(CLexer)

        source_code_window = Window(
            content=BufferControl(
                buffer=self.source_buffer,
                lexer=src_lexer,
                input_processors=[self.locals_processor],
            ),
            left_margins=[self.current_address_margin,
                          NumberedMargin()],
            right_margins=[ScrollbarMargin(display_arrows=True)],
            cursorline=True,
        )

        register_window = Window(
            content=BufferControl(buffer=self.register_buffer), width=20)

        title_text = "Welcome to the ppci debugger version {} running in prompt_toolkit {}".format(
            ppci_version, ptk_version)

        help_text = ("F4=stop F5=run F6=step F7=set breakpoint" +
                     " F8=clear breakpoint F10=exit")

        # Application layout:
        body = HSplit([
            Window(content=FormattedTextControl(text=title_text), height=1),
            VSplit([
                HSplit([
                    Frame(
                        body=source_code_window,
                        title="source-code",
                    ),
                    Window(
                        content=BufferControl(buffer=self.logs_buffer),
                        height=2,
                    ),
                ]),
                Frame(body=register_window, title="registers"),
            ]),
            Window(
                content=FormattedTextControl(self.get_status_tokens),
                height=1,
            ),
            Window(content=FormattedTextControl(help_text), height=1),
        ])
        layout = Layout(body)

        style = style_from_pygments_cls(get_style_by_name("vim"))

        log_handler = MyHandler(self.logs_buffer)
        fmt = logging.Formatter(fmt=logformat)
        log_handler.setFormatter(fmt)
        log_handler.setLevel(logging.DEBUG)
        logging.getLogger().setLevel(logging.DEBUG)
        logging.getLogger().addHandler(log_handler)

        self._event_loop = get_event_loop()

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

    def cmdloop(self):
        self.application.run()

    def get_status_tokens(self):
        tokens = []
        tokens.append(
            ("class:status", "STATUS={} ".format(self.debugger.status)))
        tokens.append(
            ("class:status", "PC={:08X} ".format(self.debugger.get_pc())))
        if self.debugger.has_symbols:
            loc = self.debugger.find_pc()
            if loc:
                filename, row = loc
                tokens.append(
                    ("class:status", "LOCATION={}:{}".format(filename, row)))
        return tokens

    def on_stop(self):
        """ Handle stopped event. """
        def callback():
            self.display_registers()
            self.highlight_source()
            self.evaluate_locals()
            self.application.invalidate()

        self._event_loop.call_soon_threadsafe(callback)

    def evaluate_locals(self):
        # Locals:
        localz = self.debugger.local_vars()
        self.locals_processor.variables.clear()
        for name, var in localz.items():
            value = self.debugger.eval_variable(var)
            var_text = "{} = {}".format(name, value)
            self.locals_processor.variables[var.loc.row] = var_text

    def has_source(self):
        return self._filename is not None

    def get_current_location(self):
        assert self.has_source()
        row = self.source_buffer.document.cursor_position_row + 1
        return self._filename, row

    def highlight_source(self):
        if self.debugger.has_symbols:
            loc = self.debugger.find_pc()
            if loc:
                filename, row = loc
                self.source_buffer.text = self.get_file_source(filename)
                self._filename = filename
                self.source_buffer.cursor_position = 3
                self.current_address_margin.current_line = row
            else:
                self.current_address_margin.current_line = None

    def display_registers(self):
        """ Update register buffer """
        registers = self.debugger.get_registers()
        register_values = self.debugger.get_register_values(registers)
        lines = ["Register values:"]
        if register_values:
            for register, value in register_values.items():
                size = register.bitsize // 4
                lines.append("{:>5.5s} : 0x{:0{sz}X}".format(str(register),
                                                             value,
                                                             sz=size))
        self.register_buffer.text = "\n".join(lines)

    def get_file_source(self, filename):
        if filename not in self.sources:
            with open(filename, "r") as f:
                source = f.read()
            self.sources[filename] = source
        return self.sources[filename]
Пример #3
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
Пример #4
0
class Notebook(Format, KeyBindings):

    app: Optional[Application]
    layout: Layout
    copied_cell: Optional[Cell]
    console: Console
    _run_notebook_nb_path: str
    cells: List[Cell]
    executing_cells: List[Cell]
    json: Dict[str, Any]
    kd: Optional[KernelDriver]
    execution_count: int
    current_cell_idx: int
    idle: Optional[asyncio.Event]

    def __init__(self, nb_path: str, no_kernel: bool = False):
        self.app = None
        self.copied_cell = None
        self.console = Console()
        self.nb_path = nb_path
        self.executing_cells = []
        if os.path.exists(nb_path):
            self.read_nb()
        else:
            self.create_nb()
        kernel_name = self.json["metadata"]["kernelspec"]["name"]
        if no_kernel:
            self.kd = None
        else:
            try:
                self.kd = KernelDriver(kernel_name=kernel_name, log=False)
                kernel_driver.driver._output_hook_default = self.output_hook
            except RuntimeError:
                self.kd = None
        self.execution_count = 1
        self.current_cell_idx = 0
        self.idle = None

    @property
    def current_cell(self):
        return self.cells[self.current_cell_idx]

    def run(self, save_path: str = ""):
        asyncio.run(self._run())
        if not save_path:
            i = self.nb_path.rfind(".")
            self._run_notebook_path = self.nb_path[:i] + "_run" + self.nb_path[
                i:]
        else:
            self._run_notebook_path = save_path
        self.save(self._run_notebook_path)

    def show(self):
        self.key_bindings = PtKeyBindings()
        self.bind_keys()
        self.create_layout()
        self.edit_mode = False
        self.app = Application(layout=self.layout,
                               key_bindings=self.key_bindings,
                               full_screen=True)
        self.focus(0)
        asyncio.run(self._show())

    def update_layout(self, idx: int):
        if self.app:
            self.create_layout()
            self.app.layout = self.layout
        self.focus(idx)

    def create_layout(self):
        inout_cells = list(
            itertools.chain.from_iterable([(
                VSplit([cell.input_prefix, cell.input]),
                VSplit([cell.output_prefix, ONE_COL, cell.output]),
            ) for cell in self.cells]))
        root_container = ScrollablePane(HSplit(inout_cells))
        self.layout = Layout(root_container)

    def focus(self, idx: int):
        if 0 <= idx < len(self.cells):
            if self.app:
                self.app.layout.focus(self.cells[idx].input_window)
            self.current_cell_idx = idx

    def exit_cell(self):
        self.edit_mode = False
        self.current_cell.update_json()
        self.current_cell.set_input_readonly()

    def enter_cell(self):
        self.edit_mode = True
        self.current_cell.set_input_editable()

    def move_up(self):
        idx = self.current_cell_idx
        if idx > 0:
            self.cells[idx -
                       1], self.cells[idx] = self.cells[idx], self.cells[idx -
                                                                         1]
            self.update_layout(idx - 1)

    def move_down(self):
        idx = self.current_cell_idx
        if idx < len(self.cells) - 1:
            self.cells[idx], self.cells[idx +
                                        1] = self.cells[idx +
                                                        1], self.cells[idx]
            self.update_layout(idx + 1)

    def clear_output(self):
        self.current_cell.clear_output()

    def markdown_cell(self):
        self.current_cell.set_as_markdown()

    def code_cell(self):
        self.current_cell.set_as_code()

    async def run_cell(self, and_select_below: bool = False):
        if self.kd:
            self.executing_cells.append(self.current_cell)
            if and_select_below:
                if self.current_cell_idx == len(self.cells) - 1:
                    self.insert_cell(self.current_cell_idx + 1)
                self.focus(self.current_cell_idx + 1)
            await self.executing_cells[-1].run()

    def cut_cell(self):
        idx = self.current_cell_idx
        self.copied_cell = self.cells.pop(idx)
        if not self.cells:
            self.cells = [Cell(self)]
        elif idx == len(self.cells):
            idx -= 1
        self.update_layout(idx)

    def copy_cell(self):
        idx = self.current_cell_idx
        self.copied_cell = self.cells[idx]

    def paste_cell(self, below=False):
        idx = self.current_cell_idx + below
        if self.copied_cell is not None:
            pasted_cell = self.copied_cell.copy()
            self.cells.insert(idx, pasted_cell)
            self.update_layout(idx)

    def insert_cell(self, below=False):
        idx = self.current_cell_idx + below
        self.cells.insert(idx, Cell(self))
        self.update_layout(idx)

    def output_hook(self, msg: Dict[str, Any]):
        msg_type = msg["header"]["msg_type"]
        content = msg["content"]
        outputs = self.executing_cells[0].json["outputs"]
        if msg_type == "stream":
            if (not outputs) or (outputs[-1]["name"] != content["name"]):
                outputs.append({
                    "name": content["name"],
                    "output_type": msg_type,
                    "text": []
                })
            outputs[-1]["text"].append(content["text"])
        elif msg_type in ("display_data", "execute_result"):
            outputs.append({
                "data": {
                    "text/plain": [content["data"].get("text/plain", "")]
                },
                "execution_count": self.execution_count,
                "metadata": {},
                "output_type": msg_type,
            })
            text = rich_print(f"Out[{self.execution_count}]:",
                              self.console,
                              style="red",
                              end="")
            self.executing_cells[
                0].output_prefix.content = FormattedTextControl(
                    text=ANSI(text))
        elif msg_type == "error":
            outputs.append({
                "ename": content["ename"],
                "evalue": content["evalue"],
                "output_type": "error",
                "traceback": content["traceback"],
            })
        else:
            return
        text, height = get_output_text_and_height(outputs, self.console)
        self.executing_cells[0].output.content = FormattedTextControl(
            text=text)
        self.executing_cells[0].output.height = height
        if self.app:
            self.app.invalidate()

    @property
    def run_notebook_path(self):
        return self._run_notebook_path

    async def _run(self):
        await self.kd.start()
        while True:
            self.executing_cells = [self.current_cell]
            await self.current_cell.run()
            if self.current_cell_idx == len(self.cells) - 1:
                break
            self.focus(self.current_cell_idx + 1)

    async def _show(self):
        if self.kd:
            asyncio.create_task(self.kd.start())
        await self.app.run_async()

    async def exit(self):
        if self.kd:
            await self.kd.stop()
        self.app.exit()

    def go_up(self):
        self.focus(self.current_cell_idx - 1)

    def go_down(self):
        self.focus(self.current_cell_idx + 1)
Пример #5
0
class Notebook(Help, Format, KeyBindings):

    app: Optional[Application]
    layout: Layout
    copied_cell: Optional[Cell]
    console: Console
    _run_notebook_nb_path: str
    cells: List[Cell]
    executing_cells: Dict[int, Cell]
    json: Dict[str, Any]
    kd: Optional[KernelDriver]
    execution_count: int
    msg_id_2_execution_count: Dict[str, int]
    current_cell_idx: int
    top_cell_idx: int
    bottom_cell_idx: int
    lexer: Optional[PygmentsLexer] = PygmentsLexer(PythonLexer)
    language: str
    kernel_name: str
    no_kernel: bool
    dirty: bool
    quitting: bool
    kernel_cwd: Path

    def __init__(
        self,
        nb_path: Path,
        kernel_cwd: Path = Path("."),
        no_kernel: bool = False,
        save_path: Optional[Path] = None,
    ):
        self.nb_path = nb_path.resolve()
        self.kernel_cwd = kernel_cwd.resolve()
        os.chdir(self.kernel_cwd)
        self.app = None
        self.copied_cell = None
        self.console = Console()
        set_console(self.console)
        self.save_path = save_path
        self.no_kernel = no_kernel
        self.executing_cells = {}
        self.top_cell_idx = 0
        self.bottom_cell_idx = -1
        self.current_cell_idx = 0
        if self.nb_path.is_file():
            self.read_nb()
        else:
            self.create_nb()
        self.dirty = False
        self.quitting = False
        self.execution_count = 0
        self.msg_id_2_execution_count = {}
        self.edit_mode = False
        self.help_mode = False

    def set_language(self):
        self.kernel_name = self.json["metadata"]["kernelspec"]["name"]
        self.language = self.json["metadata"]["kernelspec"]["language"]
        if self.language == "python":
            self.lexer = PygmentsLexer(PythonLexer)
        elif self.language == "cpp":
            self.lexer = PygmentsLexer(CppLexer)
        else:
            self.lexer = None
        if self.no_kernel:
            self.kd = None
        else:
            try:
                self.kd = KernelDriver(kernel_name=self.kernel_name, log=False)
                kernel_driver.driver._output_hook_default = self.output_hook
            except RuntimeError:
                self.kd = None

    @property
    def current_cell(self):
        return self.cells[self.current_cell_idx]

    async def run_cell(self, idx: Optional[int] = None):
        if idx is None:
            idx = self.current_cell_idx
        self.focus(idx)
        await self.current_cell.run()

    async def run_all(self):
        await self.kd.start()
        for i in range(len(self.cells)):
            await self.run_cell(i)

    def show(self):
        self.key_bindings = PtKeyBindings()
        self.bind_keys()
        self.create_layout()
        self.app = Application(layout=self.layout,
                               key_bindings=self.key_bindings,
                               full_screen=True)
        self.focus(0)
        asyncio.run(self._show())

    def update_layout(self):
        if self.app:
            self.create_layout()
            self.app.layout = self.layout

    def create_layout(self):
        inout_cells = list(
            itertools.chain.from_iterable([
                (
                    VSplit([cell.input_prefix, cell.input]),
                    VSplit([cell.output_prefix, ONE_COL, cell.output,
                            ONE_COL]),
                )
                for cell in self.cells[self.top_cell_idx:self.bottom_cell_idx +
                                       1  # noqa
                                       ]
            ]))
        nb_window = ScrollablePane(HSplit(inout_cells), show_scrollbar=False)

        def get_top_bar_text():
            text = ""
            if self.dirty:
                text += "+ "
            text += str(self.nb_path.relative_to(self.kernel_cwd))
            if self.dirty and self.quitting:
                text += (
                    " (no write since last change, please exit again to confirm, "
                    "or save your changes)")
            return text

        def get_bottom_bar_text():
            text = ""
            if self.kd and not self.no_kernel and self.kernel_name:
                if self.executing_cells:
                    kernel_status = "busy"
                else:
                    kernel_status = "idle"
                text += f"{self.kernel_name} ({kernel_status})"
            else:
                text += "[NO KERNEL]"
            text += (
                f" @ {self.kernel_cwd} - {self.current_cell_idx + 1}/{len(self.cells)}"
            )
            return text

        self.top_bar = FormattedTextToolbar(get_top_bar_text,
                                            style="#ffffff bg:#444444")
        self.bottom_bar = FormattedTextToolbar(get_bottom_bar_text,
                                               style="#ffffff bg:#444444")
        root_container = HSplit([self.top_bar, nb_window, self.bottom_bar])
        self.layout = Layout(root_container)

    def focus(self,
              idx: int,
              update_layout: bool = False,
              no_change: bool = False):
        """
        Focus on a cell.

        Parameters
        ----------
        idx : int
            Index of the cell to focus on.
        update_layout : bool, optional
            If True, force the update of the layout. Default is False.
        no_change : bool optional
            If True, the cells didn't change. Default is False.
        """
        if 0 <= idx < len(self.cells):
            if self.app:
                if self.update_visible_cells(idx, no_change) or update_layout:
                    self.update_layout()
                self.app.layout.focus(self.cells[idx].input_window)
            self.current_cell_idx = idx

    def update_visible_cells(self, idx: int, no_change: bool) -> bool:
        self.app = cast(Application, self.app)
        size = self.app.renderer.output.get_size()
        available_height = size.rows - 2  # status bars
        if idx < self.top_cell_idx or self.bottom_cell_idx == -1:
            # scroll up
            (
                self.top_cell_idx,
                self.bottom_cell_idx,
            ) = self.get_visible_cell_idx_from_top(idx, available_height)
            return True
        if idx > self.bottom_cell_idx:
            # scroll down
            (
                self.top_cell_idx,
                self.bottom_cell_idx,
            ) = self.get_visible_cell_idx_from_bottom(idx, available_height)
            return True
        if no_change:
            return False
        # there might be less or more cells, or the cells' content may have changed
        top_cell_idx_keep, bottom_cell_idx_keep = (
            self.top_cell_idx,
            self.bottom_cell_idx,
        )
        while True:
            (
                self.top_cell_idx,
                self.bottom_cell_idx,
            ) = self.get_visible_cell_idx_from_top(self.top_cell_idx,
                                                   available_height)
            if self.top_cell_idx <= idx <= self.bottom_cell_idx:
                break
            self.top_cell_idx += 1
        return not (self.top_cell_idx == top_cell_idx_keep
                    and self.bottom_cell_idx == bottom_cell_idx_keep)

    def get_visible_cell_idx_from_top(
            self, idx: int, available_height: int) -> Tuple[int, int]:
        cell_nb = -1
        for cell in self.cells[idx:]:
            available_height -= cell.get_height()
            cell_nb += 1
            if available_height <= 0:
                break
        # bottom cell may be clipped by ScrollablePane
        return idx, idx + cell_nb

    def get_visible_cell_idx_from_bottom(
            self, idx: int, available_height: int) -> Tuple[int, int]:
        cell_nb = -1
        for cell in self.cells[idx::-1]:
            available_height -= cell.get_height()
            cell_nb += 1
            if available_height <= 0:
                break
        # top cell may be clipped by ScrollablePane
        return idx - cell_nb, idx

    def exit_cell(self):
        self.edit_mode = False
        self.current_cell.update_json()
        self.current_cell.set_input_readonly()

    def enter_cell(self):
        self.edit_mode = True
        self.current_cell.set_input_editable()

    def move_up(self):
        idx = self.current_cell_idx
        if idx > 0:
            self.dirty = True
            self.cells[idx -
                       1], self.cells[idx] = self.cells[idx], self.cells[idx -
                                                                         1]
            self.focus(idx - 1, update_layout=True)

    def move_down(self):
        idx = self.current_cell_idx
        if idx < len(self.cells) - 1:
            self.dirty = True
            self.cells[idx], self.cells[idx +
                                        1] = self.cells[idx +
                                                        1], self.cells[idx]
            self.focus(idx + 1, update_layout=True)

    def clear_output(self):
        self.current_cell.clear_output()

    def markdown_cell(self):
        self.current_cell.set_as_markdown()

    def code_cell(self):
        self.current_cell.set_as_code()

    def raw_cell(self):
        self.current_cell.set_as_raw()

    async def queue_run_cell(self, and_select_below: bool = False):
        if self.kd:
            cell = self.current_cell
            if and_select_below:
                if self.current_cell_idx == len(self.cells) - 1:
                    self.insert_cell(self.current_cell_idx + 1)
                self.focus(self.current_cell_idx + 1)
            await cell.run()

    def cut_cell(self, idx: Optional[int] = None):
        self.dirty = True
        if idx is None:
            idx = self.current_cell_idx
        self.copied_cell = self.cells.pop(idx)
        if not self.cells:
            self.cells = [Cell(self)]
        elif idx == len(self.cells):
            idx -= 1
        self.focus(idx, update_layout=True)

    def copy_cell(self, idx: Optional[int] = None):
        if idx is None:
            idx = self.current_cell_idx
        idx = self.current_cell_idx
        self.copied_cell = self.cells[idx]

    def paste_cell(self, idx: Optional[int] = None, below=False):
        if self.copied_cell is not None:
            self.dirty = True
            if idx is None:
                idx = self.current_cell_idx + below
            pasted_cell = self.copied_cell.copy()
            self.cells.insert(idx, pasted_cell)
            self.focus(idx, update_layout=True)

    def insert_cell(self, idx: Optional[int] = None, below=False):
        self.dirty = True
        if idx is None:
            idx = self.current_cell_idx + below
        self.cells.insert(idx, Cell(self))
        self.focus(idx, update_layout=True)

    def output_hook(self, msg: Dict[str, Any]):
        msg_id = msg["parent_header"]["msg_id"]
        execution_count = self.msg_id_2_execution_count[msg_id]
        msg_type = msg["header"]["msg_type"]
        content = msg["content"]
        outputs = self.executing_cells[execution_count].json["outputs"]
        if msg_type == "stream":
            if (not outputs) or (outputs[-1]["name"] != content["name"]):
                outputs.append({
                    "name": content["name"],
                    "output_type": msg_type,
                    "text": []
                })
            outputs[-1]["text"].append(content["text"])
        elif msg_type in ("display_data", "execute_result"):
            outputs.append({
                "data": {
                    "text/plain": [content["data"].get("text/plain", "")]
                },
                "execution_count": execution_count,
                "metadata": {},
                "output_type": msg_type,
            })
            text = rich_print(f"Out[{execution_count}]:", style="red", end="")
            self.executing_cells[
                execution_count].output_prefix.content = FormattedTextControl(
                    text=ANSI(text))
        elif msg_type == "error":
            outputs.append({
                "ename": content["ename"],
                "evalue": content["evalue"],
                "output_type": "error",
                "traceback": content["traceback"],
            })
        else:
            return
        text, height = get_output_text_and_height(outputs)
        self.executing_cells[
            execution_count].output.content = FormattedTextControl(text=text)
        height_keep = self.executing_cells[execution_count].output.height
        self.executing_cells[execution_count].output.height = height
        if self.app and height_keep != height:
            # height has changed
            self.focus(self.current_cell_idx, update_layout=True)
            self.app.invalidate()

    async def _show(self):
        if self.kd:
            asyncio.create_task(self.kd.start())
        await self.app.run_async()

    async def exit(self):
        if self.dirty and not self.quitting:
            self.quitting = True
            return
        if self.kd:
            await self.kd.stop()
        self.app.exit()

    def go_up(self):
        self.focus(self.current_cell_idx - 1, no_change=True)

    def go_down(self):
        self.focus(self.current_cell_idx + 1, no_change=True)
Пример #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'))
Пример #7
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
Пример #8
0
async def init():
    global root
    global chat_container
    global app
    global log_buf
    global log_win
    global input_buf
    global input_win
    global convos
    global status_bar
    global status_label
    global client
    global convo_stack
    global websocket
    global app
    global uri
    global websocket
    global ws_handler

    uri = "ws://localhost:15555"
    ws_handler = handler(uri)
    await ws_handler.connect()

    # message area
    log_buf = Buffer(document=Document())
    log_win = Window(BufferControl(log_buf), wrap_lines=True)

    # input area
    input_buf = Buffer(document=Document())
    input_win = Window(BufferControl(input_buf), height=1, wrap_lines=True)

    # status bar
    status_bar = FormattedTextToolbar(
        text=HTML("<b>Chatting with: Loading </b>"),
        style="bg:ansired fg:ansiblack")
    status_label = Label(text="[ 00:29 ] ", width=10)

    # call backs
    input_buf.accept_handler = accept_message
    input_buf.on_text_changed += resize_input
    log_buf.on_text_changed += auto_scroll
    convos = convo_list_widget()

    chat_container = HSplit(
        [log_win, status_bar,
         VSplit([status_label, input_win])])

    root = VSplit([
        convos,
        chat_container,
    ])

    style = Style.from_dict(
        {"select-box cursor-line": "nounderline bg:ansired fg:ansiwhite"})

    app = Application(editing_mode=EditingMode.VI,
                      key_bindings=kb,
                      layout=Layout(chat_container),
                      full_screen=True,
                      style=style)
    app.invalidate()
    app.layout.focus(input_buf)
    ViState._input_mode = InputMode.INSERT
    ViState.input_mode = property(get_input_mode, set_input_mode)

    asyncio.ensure_future(ws_handler.listen())
    asyncio.ensure_future(ws_handler.command(('get_convo', 'all')))

    auto_scroll(log_buf)
    await app.run_async()
Пример #9
0
class ImbTui:
    def __init__(self):
        self.init_done = asyncio.Event()

    async def start_ui(self):
        kb = KeyBindings()

        @kb.add('escape')
        def _(event):
            for t in asyncio.Task.all_tasks():
                if 'Imb.main()' in str(t) and not t.done():
                    t.cancel()

        # Allow member functions to access this frame to allow switching screens
        self.app_frame = Frame(title='Opsani Intelligent Manifest Builder',
                               body=Window())
        self.app = Application(
            full_screen=True,
            key_bindings=kb,
            layout=Layout(HSplit([self.app_frame,
                                  Label('Press ESC to exit')])))
        self.init_done.set()
        await self.app.run_async(set_exception_handler=False)

    async def stop_ui(self):
        self.app.exit()

    async def prompt_yn(self,
                        title,
                        prompt,
                        disable_back=False,
                        allow_other=False,
                        other_button_text="Other"):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        def yes_handler():
            result.value = True
            input_done.set()

        def no_handler():
            result.value = False
            input_done.set()

        def back_handler():
            result.back_selected = True
            input_done.set()

        def other_handler():
            result.other_selected = True
            input_done.set()

        buttons = [
            Button(text="Yes", handler=yes_handler),
            Button(text="No", handler=no_handler)
        ]
        if not disable_back:
            buttons.append(Button(text="Back", handler=back_handler))

        if allow_other:
            buttons.append(
                Button(text=other_button_text, handler=other_handler))

        yn_dialog = Dialog(
            title=title,
            body=Window(FormattedTextControl(prompt),
                        height=1,
                        align=WindowAlign.CENTER),
            buttons=buttons,
            modal=False,
        )
        # disable a_reverse style applied to dialogs
        yn_dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            Window(),
            yn_dialog,
            Window(),
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    # prompt-toolkit supports line wrapping but it disregards breaking of words across lines
    # In cases where the text will span multiple lines, it should be divided up into an
    # array of prompt lines where each line should be short enough to fit on the screen
    async def prompt_ok(self, title, prompt: Union[str, Iterable[str]]):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        dialog_body = []
        if isinstance(prompt, str):
            dialog_body.append(
                Window(FormattedTextControl(prompt),
                       height=1,
                       align=WindowAlign.CENTER))
        else:
            for line in prompt:
                dialog_body.append(
                    Window(FormattedTextControl(line),
                           height=1,
                           align=WindowAlign.CENTER))

        def ok_handler():
            result.value = True
            input_done.set()

        def back_handler():
            result.back_selected = True
            input_done.set()

        ok_dialog = Dialog(
            title=title,
            body=HSplit(dialog_body),
            buttons=[
                Button(text="Ok", handler=ok_handler),
                Button(text="Back", handler=back_handler),
            ],
            modal=False,
        )
        # disable a_reverse style applied to dialogs
        ok_dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            Window(),
            ok_dialog,
            Window(),
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def long_prompt_text_input(self,
                                     title,
                                     prompt: Union[str, Iterable[str]],
                                     initial_text='',
                                     allow_other=False):
        'prompt for single text input with a multi-line prompt'
        result = ImbTuiResult()
        input_done = asyncio.Event()

        def accept(buf) -> bool:
            get_app().layout.focus(ok_button)
            return True  # Keep text.

        dialog_body = []
        if isinstance(prompt, str):
            dialog_body.append(
                Window(FormattedTextControl(prompt),
                       height=1,
                       align=WindowAlign.CENTER))
        else:
            for line in prompt:
                dialog_body.append(
                    Window(FormattedTextControl(line),
                           height=1,
                           align=WindowAlign.CENTER))

        text_field = TextArea(text=initial_text,
                              multiline=False,
                              accept_handler=accept)
        dialog_body.append(text_field)

        def ok_handler():
            result.value = text_field.text
            input_done.set()

        def back_handler():
            result.back_selected = True
            input_done.set()

        ok_button = Button(text='Ok', handler=ok_handler)
        dialog = Dialog(
            title=title,
            body=HSplit(dialog_body),
            buttons=[
                ok_button,
                Button(text="Back", handler=back_handler),
            ],
            modal=False,
        )
        # disable a_reverse style applied to dialogs
        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            Window(),
            dialog,
            Window(),
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def prompt_text_input(self,
                                title,
                                prompts,
                                allow_other=False,
                                other_button_text="Other",
                                ok_button_text="Ok"):
        result = ImbTuiResult()
        input_done = asyncio.Event()
        text_fields = []
        dialog_hsplit_content = []

        def accept(buf) -> bool:
            get_app().layout.focus(ok_button)
            return True  # Keep text.

        def ok_handler() -> None:
            if len(prompts) == 1:
                result.value = text_fields[0].text
            else:
                result.value = tuple(t.text for t in text_fields)
            input_done.set()

        def back_handler() -> None:
            result.back_selected = True
            input_done.set()

        def other_handler() -> None:
            if len(prompts) == 1:
                result.value = text_fields[0].text
            else:
                result.value = tuple(t.text for t in text_fields)
            result.other_selected = True
            input_done.set()

        ok_button = Button(text=ok_button_text, handler=ok_handler
                           )  # capture ref to allow accept handler to focus it
        buttons = [ok_button, Button(text='Back', handler=back_handler)]
        if allow_other:
            buttons.append(
                Button(text=other_button_text, handler=other_handler))

        for p in prompts:
            text_field = TextArea(text=p.get('initial_text', ''),
                                  multiline=False,
                                  accept_handler=accept)
            text_fields.append(text_field)
            dialog_hsplit_content.extend([
                Window(FormattedTextControl(text=p['prompt']),
                       align=WindowAlign.CENTER,
                       dont_extend_height=True), text_field
            ])

        dialog = Dialog(
            title=title,
            body=HSplit(
                dialog_hsplit_content,
                padding=Dimension(preferred=1, max=1),
            ),
            buttons=buttons,
            modal=False,
        )
        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            Window(),
            dialog,
            Window(),
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def prompt_multiline_text_input(self,
                                          title,
                                          prompt: Union[str, Iterable[str]],
                                          initial_text=''):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        dialog_body = []
        if isinstance(prompt, str):
            dialog_body.append(
                Window(FormattedTextControl(prompt),
                       height=1,
                       align=WindowAlign.CENTER))
        else:
            for line in prompt:
                dialog_body.append(
                    Window(FormattedTextControl(line),
                           height=1,
                           align=WindowAlign.CENTER))

        textfield = TextArea(text=initial_text, multiline=True, scrollbar=True)
        dialog_body.append(textfield)

        def ok_handler() -> None:
            result.value = textfield.text
            input_done.set()

        def back_handler() -> None:
            result.back_selected = True
            input_done.set()

        ok_button = Button(text='Ok', handler=ok_handler)
        back_button = Button(text='Back', handler=back_handler)

        dialog = Dialog(
            title=title,
            body=HSplit(dialog_body, padding=Dimension(preferred=1, max=1)),
            buttons=[ok_button, back_button],
            modal=False,
        )

        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            dialog,
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def prompt_multiline_text_output(self, title, text=''):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        def ok_handler() -> None:
            input_done.set()

        def back_handler() -> None:
            result.back_selected = True
            input_done.set()

        ok_button = Button(text='Ok', handler=ok_handler)
        back_button = Button(text='Back', handler=back_handler)

        dialog = Dialog(
            title=title,
            body=TextArea(text=text,
                          wrap_lines=True,
                          multiline=True,
                          scrollbar=True,
                          read_only=True),
            buttons=[ok_button, back_button],
            modal=False,
        )

        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            dialog,
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def prompt_radio_list(self, values, title, header, allow_other=True):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        def ok_handler() -> None:
            result.value = radio_list.current_value
            input_done.set()

        def back_handler() -> None:
            result.back_selected = True
            input_done.set()

        def other_handler() -> None:
            result.other_selected = True
            input_done.set()

        buttons = [
            Button(text='Ok', handler=ok_handler),
            Button(text='Back', handler=back_handler),
        ]
        if allow_other:
            buttons.append(Button(text='Other', handler=other_handler))

        radio_list = RadioList(list(enumerate(values)))
        dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=HTML("    <b>{}</b>".format(header)),
                      dont_extend_height=True),
                radio_list,
            ],
                        padding=1),
            buttons=buttons,
            modal=False,
        )
        # disable a_reverse style applied to dialogs
        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([
            # Window(FormattedTextControl(HTML('<b>Kubernetes Config</b>')), align=WindowAlign.CENTER, height=1), # TODO: screen header
            Window(),
            dialog,
            Window()
        ])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result

    async def prompt_check_list(self, values, title, header, allow_other=True):
        result = ImbTuiResult()
        input_done = asyncio.Event()

        def ok_handler() -> None:
            result.value = cb_list.current_values
            input_done.set()

        def back_handler() -> None:
            result.back_selected = True
            input_done.set()

        def other_handler() -> None:
            result.other_selected = True
            input_done.set()

        buttons = [
            Button(text='Ok', handler=ok_handler),
            Button(text='Back', handler=back_handler),
        ]
        if allow_other:
            buttons.append(Button(text='Other', handler=other_handler))

        cb_list = CheckboxList(list(enumerate(values)))
        dialog = Dialog(
            title=title,
            body=HSplit([
                Label(text=HTML("    <b>{}</b>".format(header)),
                      dont_extend_height=True),
                cb_list,
            ],
                        padding=1),
            buttons=buttons,
            modal=False,
        )
        # disable a_reverse style applied to dialogs
        dialog.container.container.content.style = ""
        self.app_frame.body = HSplit([Window(), dialog, Window()])
        self.app.invalidate()
        self.app.layout.focus(self.app_frame)
        await input_done.wait()
        return result