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()
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]
class FullNodeUI: """ Full node UI instance. Displays node state, blocks, and connections. Calls parent_close_cb when the full node is closed. Uses store, blockchain, and connections, to display relevant information. The UI is updated periodically. """ def __init__( self, store: FullNodeStore, blockchain: Blockchain, server: ChiaServer, port: int, parent_close_cb: Callable, ): self.port: int = port self.store: FullNodeStore = store self.blockchain: Blockchain = blockchain self.node_server: ChiaServer = server self.connections: PeerConnections = server.global_connections self.logs: List[logging.LogRecord] = [] self.app: Optional[Application] = None self.closed: bool = False self.num_blocks: int = 10 self.num_top_block_pools: int = 5 self.top_winners: List[Tuple[uint64, bytes32]] = [] self.our_winners: List[Tuple[uint64, bytes32]] = [] self.prev_route: str = "home/" self.route: str = "home/" self.focused: bool = False self.parent_close_cb = parent_close_cb self.kb = self.setup_keybindings() self.style = Style([("error", "#ff0044")]) self.pool_pks: List[PublicKey] = [] key_config_filename = os.path.join(ROOT_DIR, "config", "keys.yaml") if os.path.isfile(key_config_filename): config = safe_load(open(key_config_filename, "r")) self.pool_pks = [ PrivateKey.from_bytes(bytes.fromhex(ce)).get_public_key() for ce in config["pool_sks"] ] self.draw_initial() self.app = Application( style=self.style, layout=self.layout, full_screen=True, key_bindings=self.kb, mouse_support=True, ) self.closed = False self.update_ui_task = asyncio.get_running_loop().create_task( self.update_ui()) self.update_data_task = asyncio.get_running_loop().create_task( self.update_data()) def close(self): # Closes this instance of the UI if not self.closed: self.closed = True self.route = "home/" if self.app: self.app.exit(0) def stop(self): # Closes this instance of the UI, and call parent close, which closes # all other instances, and shuts down the full node. self.close() self.parent_close_cb() def setup_keybindings(self) -> KeyBindings: kb = KeyBindings() kb.add("tab")(focus_next) kb.add("s-tab")(focus_previous) kb.add("down")(focus_next) kb.add("up")(focus_previous) kb.add("right")(focus_next) kb.add("left")(focus_previous) @kb.add("c-c") def exit_(event): self.close() return kb def draw_initial(self): search_field = SearchToolbar() self.empty_row = TextArea(focusable=False, height=1) # home/ self.loading_msg = Label(text=f"Initializing UI....") self.server_msg = Label(text=f"Server running on port {self.port}.") self.syncing = TextArea(focusable=False, height=1) self.current_heads_label = TextArea(focusable=False, height=1) self.lca_label = TextArea(focusable=False, height=1) self.difficulty_label = TextArea(focusable=False, height=1) self.ips_label = TextArea(focusable=False, height=1) self.total_iters_label = TextArea(focusable=False, height=2) self.con_rows = [] self.displayed_cons = [] self.latest_blocks: List[HeaderBlock] = [] self.connections_msg = Label(text=f"Connections") self.connection_rows_vsplit = Window() self.add_connection_msg = Label(text=f"Add a connection ip:port") self.add_connection_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, search_field=search_field, ) self.add_connection_field.accept_handler = self.async_to_sync( self.add_connection) self.latest_blocks_msg = Label(text=f"Latest blocks") self.latest_blocks_labels = [ Button(text="block") for _ in range(self.num_blocks) ] self.search_block_msg = Label(text=f"Search block by hash") self.search_block_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, search_field=search_field, ) self.search_block_field.accept_handler = self.async_to_sync( self.search_block) self.top_block_pools_msg = Label(text=f"Top block pools") self.top_block_pools_labels = [ Label(text="Top block pool") for _ in range(self.num_top_block_pools) ] self.our_pools_msg = Label(text=f"Our pool winnings") self.our_pools_labels = [ Label(text="Our winnings") for _ in range(len(self.pool_pks)) ] self.close_ui_button = Button("Close UI", handler=self.close) self.quit_button = Button("Stop node and close UI", handler=self.stop) self.error_msg = Label(style="class:error", text=f"") # block/ self.block_msg = Label(text=f"Block") self.block_label = TextArea(focusable=True, scrollbar=True, focus_on_click=True) self.back_button = Button(text="Back", handler=self.change_route_handler("home/")) self.challenge_msg = Label(text=f"Block Header") self.challenge = TextArea(focusable=False) body = HSplit([self.loading_msg, self.server_msg], height=D(), width=D()) self.content = Frame(title="Chia Full Node", body=body) self.layout = Layout(VSplit([self.content], height=D(), width=D())) def change_route_handler(self, route): def change_route(): self.prev_route = self.route self.route = route self.focused = False self.error_msg.text = "" return change_route def async_to_sync(self, coroutine): def inner(buff): asyncio.get_running_loop().create_task(coroutine(buff.text)) return inner async def search_block(self, text: str): try: block = await self.store.get_block(bytes.fromhex(text)) except ValueError: self.error_msg.text = "Enter a valid hex block hash" return if block is not None: self.change_route_handler(f"block/{text}")() else: self.error_msg.text = "Block not found" async def add_connection(self, text: str): if ":" not in text: self.error_msg.text = ( "Enter a valid IP and port in the following format: 10.5.4.3:8000" ) return else: ip, port = ":".join(text.split(":")[:-1]), text.split(":")[-1] target_node: PeerInfo = PeerInfo(ip, uint16(int(port))) log.error(f"Want to connect to {ip}, {port}") if not (await self.node_server.start_client(target_node, None)): self.error_msg.text = f"Failed to connect to {ip}:{port}" async def get_latest_blocks(self, heads: List[HeaderBlock]) -> List[HeaderBlock]: added_blocks: List[HeaderBlock] = [] while len(added_blocks) < self.num_blocks and len(heads) > 0: heads = sorted(heads, key=lambda b: b.height, reverse=True) max_block = heads[0] if max_block not in added_blocks: added_blocks.append(max_block) heads.remove(max_block) prev: Optional[HeaderBlock] = self.blockchain.header_blocks.get( max_block.prev_header_hash, None) if prev is not None: heads.append(prev) return added_blocks async def draw_home(self): connections = [c for c in self.connections.get_connections()] if collections.Counter(connections) != collections.Counter( self.displayed_cons): new_con_rows = [] for con in connections: con_str = f"{NodeType(con.connection_type).name} {con.get_peername()} {con.node_id.hex()[:10]}..." con_label = Label(text=con_str) def disconnect(c): def inner(): self.connections.close(c) self.layout.focus(self.quit_button) return inner disconnect_button = Button("Disconnect", handler=disconnect(con)) row = VSplit([con_label, disconnect_button]) new_con_rows.append(row) self.displayed_cons = connections self.con_rows = new_con_rows if len(self.con_rows) > 0: self.layout.focus(self.con_rows[0]) else: self.layout.focus(self.quit_button) if len(self.con_rows): new_con_rows = HSplit(self.con_rows) else: new_con_rows = Window(width=D(), height=0) if await self.store.get_sync_mode(): max_height = -1 for _, block in await self.store.get_potential_tips_tuples(): if block.height > max_height: max_height = block.height if max_height >= 0: self.syncing.text = f"Syncing up to {max_height}" else: self.syncing.text = f"Syncing" else: self.syncing.text = "Not syncing" heads: List[HeaderBlock] = self.blockchain.get_current_tips() lca_block: FullBlock = self.blockchain.lca_block if lca_block.height > 0: difficulty = await self.blockchain.get_next_difficulty( lca_block.prev_header_hash) ips = await self.blockchain.get_next_ips(lca_block.prev_header_hash ) else: difficulty = await self.blockchain.get_next_difficulty( lca_block.header_hash) ips = await self.blockchain.get_next_ips(lca_block.header_hash) total_iters = lca_block.header_block.challenge.total_iters new_block_labels = [] for i, b in enumerate(self.latest_blocks): self.latest_blocks_labels[i].text = ( f"{b.height}:{b.header_hash}" f" {'LCA' if b.header_hash == lca_block.header_hash else ''}" f" {'TIP' if b.header_hash in [h.header_hash for h in heads] else ''}" ) self.latest_blocks_labels[i].handler = self.change_route_handler( f"block/{b.header_hash}") new_block_labels.append(self.latest_blocks_labels[i]) top_block_pools_labels = self.top_block_pools_labels if len(self.top_winners) > 0: new_top_block_pools_labels = [] for i, (winnings, pk) in enumerate(self.top_winners): self.top_block_pools_labels[ i].text = f"Public key {pk.hex()}: {winnings/1000000000000} chias." new_top_block_pools_labels.append( self.top_block_pools_labels[i]) top_block_pools_labels = new_top_block_pools_labels our_pools_labels = self.our_pools_labels if len(self.our_winners) > 0: new_our_pools_labels = [] for i, (winnings, pk) in enumerate(self.our_winners): self.our_pools_labels[ i].text = f"Public key {pk.hex()}: {winnings/(1000000000000)} chias." new_our_pools_labels.append(self.our_pools_labels[i]) our_pools_labels = new_our_pools_labels self.lca_label.text = f"Current least common ancestor {lca_block.header_hash} height {lca_block.height}" self.current_heads_label.text = "Heights of tips: " + str( [h.height for h in heads]) self.difficulty_label.text = f"Current difficuty: {difficulty}" self.ips_label.text = f"Current VDF iterations per second: {ips}" self.total_iters_label.text = f"Total iterations since genesis: {total_iters}" try: if not self.focused: self.layout.focus(self.close_ui_button) self.focused = True except ValueError: # Not yet in layout pass return HSplit( [ self.server_msg, self.syncing, self.lca_label, self.current_heads_label, self.difficulty_label, self.ips_label, self.total_iters_label, Window(height=1, char="-", style="class:line"), self.connections_msg, new_con_rows, Window(height=1, char="-", style="class:line"), self.add_connection_msg, self.add_connection_field, Window(height=1, char="-", style="class:line"), self.latest_blocks_msg, *new_block_labels, Window(height=1, char="-", style="class:line"), self.search_block_msg, self.search_block_field, Window(height=1, char="-", style="class:line"), self.top_block_pools_msg, *top_block_pools_labels, Window(height=1, char="-", style="class:line"), self.our_pools_msg, *our_pools_labels, Window(height=1, char="-", style="class:line"), self.close_ui_button, self.quit_button, self.error_msg, ], width=D(), height=D(), ) async def draw_block(self): block_hash: str = self.route.split("block/")[1] async with self.store.lock: block: Optional[FullBlock] = await self.store.get_block( bytes32(bytes.fromhex(block_hash))) if block is not None: self.block_msg.text = f"Block {str(block.header_hash)}" if self.block_label.text != str(block): self.block_label.text = str(block) else: self.block_label.text = f"Block hash {block_hash} not found" try: if not self.focused: self.layout.focus(self.back_button) self.focused = True except ValueError: # Not yet in layout pass return HSplit([self.block_msg, self.block_label, self.back_button], width=D(), height=D()) async def update_ui(self): try: while not self.closed: if self.route.startswith("home/"): self.content.body = await self.draw_home() elif self.route.startswith("block/"): self.content.body = await self.draw_block() if self.app and not self.app.invalidated: self.app.invalidate() await asyncio.sleep(0.25) except concurrent.futures._base.CancelledError as e: log.warn(f"Cancelled error in UI: {type(e)}: {e}") except Exception as e: log.warn(f"Exception in UI update_ui {type(e)}: {e}") raise e async def update_data(self): try: while not self.closed: heads: List[HeaderBlock] = self.blockchain.get_current_tips() self.latest_blocks = await self.get_latest_blocks(heads) header_block = heads[0] coin_balances = { bytes(header_block.proof_of_space.pool_pubkey): calculate_block_reward(header_block.height) } while header_block.height != 0: header_block = self.blockchain.header_blocks[ header_block.prev_header_hash] pool_pk = bytes(header_block.proof_of_space.pool_pubkey) if pool_pk not in coin_balances: coin_balances[pool_pk] = 0 coin_balances[pool_pk] += calculate_block_reward( header_block.height) self.top_winners = sorted( [(rewards, key) for key, rewards in coin_balances.items()], reverse=True, )[:self.num_top_block_pools] self.our_winners = [ (coin_balances[bytes(pk)], bytes(pk)) if bytes(pk) in coin_balances else (0, bytes(pk)) for pk in self.pool_pks ] await asyncio.sleep(5) except concurrent.futures._base.CancelledError as e: log.warn(f"Cancelled error in UI: {type(e)}: {e}") except Exception as e: log.warn(f"Exception in UI update_data {type(e)}: {e}") raise e async def await_closed(self): await self.update_ui_task await self.update_data_task
class 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)
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)
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'))
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
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()
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