class Listener: def __init__(self, app: 'App'): self._app = app self._running = False self._loop: Optional[asyncio.AbstractEventLoop] = None self._queue: Optional[asyncio.Queue] = None self._thread: Optional[Thread] = None self._prompt_app: Optional[Application] = None def start(self): self._running = True self._queue = asyncio.Queue(1) self._loop = asyncio.get_event_loop() self._loop.create_task(self._execute()) kb = KeyBindings() kb.add(Keys.Escape)(self._handle) kb.add(Keys.ControlC)(self._exit) self._prompt_app = Application(layout=Layout(Window()), key_bindings=kb) self._thread = Thread(target=self._prompt_app.run) self._thread.start() def stop(self): self._running = False if self._prompt_app: self._prompt_app.exit() self._prompt_app = None def _handle(self, event: KeyPressEvent): async def _put_threadsafe(): try: key = event.key_sequence[0].key self._queue.put_nowait(key) except asyncio.QueueFull: pass asyncio.run_coroutine_threadsafe(_put_threadsafe(), self._loop) def _exit(self, event: KeyPressEvent): self.stop() self._app.close() async def _execute(self): key = await self._queue.get() self.stop() try: handler = Handler() await handler.handle(key, self._app) finally: self.start()
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 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 PlayListEditor: def __init__(self, playlist: LocalFilePlaylist, editable=False) -> None: self.playlist: LocalFilePlaylist = playlist self.editable = False self.text_field = None self.application = None self.key_bindings = None self.layout = None self.play_info_dialog = None self.skip_info_dialog = None self.show_status_bar = True self.focus_index = 0 def run(self): if self.application is not None: self.application.run() def is_show_status_bar(self): return self.show_status_bar def create_key_bindings(self): kb = KeyBindings() @kb.add('c-q') def _(event): self.exit() kb.add("tab")(focus_next) self.key_bindings = kb def create_content(self): # 文本编辑器 text_editor = self.create_text_editor() # 播放列表属性编辑器 property_editor = self.create_property_editor() body = HSplit([ HSplit( [ property_editor, Label(' '), Window(height=1, char="-", style="class:line"), text_editor, ], height=D(), ), ConditionalContainer( content=VSplit( [ Window(FormattedTextControl(self.get_statusbar_text), style="class:status"), Window( FormattedTextControl( self.get_statusbar_right_text), style="class:status.right", width=9, align=WindowAlign.RIGHT, ), ], height=1, ), filter=Condition(lambda: self.is_show_status_bar()), ), ]) self.create_key_bindings() root_container = MenuContainer( body=body, menu_items=[ MenuItem( "File", children=[ MenuItem("Exit", handler=self.exit), ], ), ], floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ), ], key_bindings=self.key_bindings, ) style = Style.from_dict({ "status": "reverse", "shadow": "bg:#440044", }) self.layout = Layout(root_container, focused_element=self.text_field) self.application = Application( layout=self.layout, enable_page_navigation_bindings=True, style=style, mouse_support=True, full_screen=True, ) def create_property_editor(self): current_file_path = self.playlist.media_list[ self.playlist.current_index] _, current_file_name = os.path.split(current_file_path) self.play_info_dialog = Dialog( modal=False, title="播放记录", body=HSplit([ Label(''), Label(' 播放文件'), Button(f'{current_file_name}', ), Label(' 播放位置'), Button(f'{self.playlist.current_pos}', ), Label(''), ], width=38, padding=1)) self.skip_info_dialog = Dialog( modal=False, title="设置", body=HSplit([ Label(''), Label(' 跳过片头'), Button(f'{self.playlist.skip_head}', ), Label(' 跳过片尾'), Button(f'{self.playlist.skip_tail}', ), Label(''), ], width=38, padding=1)) left_window = VSplit([ self.play_info_dialog, self.skip_info_dialog, ], width=40, padding=2) return left_window def create_text_editor(self): search_toolbar = SearchToolbar() text = '' for file_path in self.playlist.media_list: text += file_path + '\n' self.text_field = TextArea( text=text, read_only=True, #lexer=DynamicLexer( # lambda: PygmentsLexer.from_filename( # ApplicationState.current_path or ".txt", sync_from_start=False # ) #), scrollbar=True, line_numbers=True, search_field=search_toolbar, ) text_editor = HSplit([self.text_field, search_toolbar]) return text_editor def exit(self): if self.application.is_running: self.application.exit() def get_statusbar_text(self): return " Press ctrl-q to exit. " def get_statusbar_right_text(self): return " {}:{} ".format( self.text_field.document.cursor_position_row + 1, self.text_field.document.cursor_position_col + 1, )
class FullNodeUI: """ Full node UI instance. Displays node state, blocks, and connections. Calls parent_close_cb when the full node is closed. Uses the RPC client to fetch data from a full node and to display relevant information. The UI is updated periodically. """ def __init__(self, parent_close_cb: Callable, rpc_client: RpcClient): self.rpc_client = rpc_client self.app: Optional[Application] = None self.data_initialized = False self.block = None self.closed: bool = False self.num_blocks: int = 10 self.num_top_block_pools: int = 10 self.top_winners: List[Tuple[uint64, bytes32]] = [] self.our_winners: List[Tuple[uint64, bytes32]] = [] self.prev_route: str = "home/" self.route: str = "home/" self.focused: bool = False self.parent_close_cb = parent_close_cb self.kb = self.setup_keybindings() self.style = Style([("error", "#ff0044")]) self.pool_pks: List[PublicKey] = [] key_config_filename = os.path.join(ROOT_DIR, "config", "keys.yaml") if os.path.isfile(key_config_filename): config = safe_load(open(key_config_filename, "r")) self.pool_pks = [ PrivateKey.from_bytes(bytes.fromhex(ce)).get_public_key() for ce in config["pool_sks"] ] self.draw_initial() self.app = Application( style=self.style, layout=self.layout, full_screen=True, key_bindings=self.kb, mouse_support=True, ) self.closed = False self.update_ui_task = asyncio.get_running_loop().create_task( self.update_ui()) self.update_data_task = asyncio.get_running_loop().create_task( self.update_data()) def close(self): # Closes this instance of the UI if not self.closed: self.closed = True self.route = "home/" if self.app: self.app.exit(0) def stop(self): # Closes this instance of the UI, and call parent close, which closes # all other instances, and shuts down the full node. self.close() self.parent_close_cb(True) def setup_keybindings(self) -> KeyBindings: kb = KeyBindings() kb.add("tab")(focus_next) kb.add("s-tab")(focus_previous) kb.add("down")(focus_next) kb.add("up")(focus_previous) kb.add("right")(focus_next) kb.add("left")(focus_previous) @kb.add("c-c") def exit_(event): self.close() return kb def draw_initial(self): search_field = SearchToolbar() self.empty_row = TextArea(focusable=False, height=1) # home/ self.loading_msg = Label(text=f"Initializing UI....") self.syncing = TextArea(focusable=False, height=1) self.current_heads_label = TextArea(focusable=False, height=1) self.lca_label = TextArea(focusable=False, height=1) self.difficulty_label = TextArea(focusable=False, height=1) self.ips_label = TextArea(focusable=False, height=1) self.total_iters_label = TextArea(focusable=False, height=2) self.con_rows = [] self.displayed_cons = set() self.latest_blocks: List[HeaderBlock] = [] self.connections_msg = Label(text=f"Connections") self.connection_rows_vsplit = Window() self.add_connection_msg = Label(text=f"Add a connection ip:port") self.add_connection_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, search_field=search_field, ) self.add_connection_field.accept_handler = self.async_to_sync( self.add_connection) self.latest_blocks_msg = Label(text=f"Latest blocks") self.latest_blocks_labels = [ Button(text="block") for _ in range(self.num_blocks) ] self.search_block_msg = Label(text=f"Search block by hash") self.search_block_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, search_field=search_field, ) self.search_block_field.accept_handler = self.async_to_sync( self.search_block) self.top_block_pools_msg = Label(text=f"Top block pools") self.top_block_pools_labels = [ Label(text="Top block pool") for _ in range(self.num_top_block_pools) ] self.our_pools_msg = Label(text=f"Our pool winnings") self.our_pools_labels = [ Label(text="Our winnings") for _ in range(len(self.pool_pks)) ] self.close_ui_button = Button("Close UI", handler=self.close) self.quit_button = Button("Stop node and close UI", handler=self.stop) self.error_msg = Label(style="class:error", text=f"") # block/ self.block_msg = Label(text=f"Block") self.block_label = TextArea(focusable=True, scrollbar=True, focus_on_click=True) self.back_button = Button(text="Back", handler=self.change_route_handler("home/")) self.challenge_msg = Label(text=f"Block Header") self.challenge = TextArea(focusable=False) body = HSplit([self.loading_msg], height=D(), width=D()) self.content = Frame(title="Chia Full Node", body=body) self.layout = Layout(VSplit([self.content], height=D(), width=D())) def change_route_handler(self, route): def change_route(): self.prev_route = self.route self.route = route self.focused = False self.error_msg.text = "" return change_route def async_to_sync(self, coroutine): def inner(buff=None): if buff is None: asyncio.get_running_loop().create_task(coroutine()) else: asyncio.get_running_loop().create_task(coroutine(buff.text)) return inner async def search_block(self, text: str): try: block = await self.rpc_client.get_block(bytes.fromhex(text)) except ValueError: self.error_msg.text = "Enter a valid hex block hash" return if block is not None: self.change_route_handler(f"block/{text}")() else: self.error_msg.text = "Block not found" async def add_connection(self, text: str): if ":" not in text: self.error_msg.text = ( "Enter a valid IP and port in the following format: 10.5.4.3:8000" ) return else: ip, port = ":".join(text.split(":")[:-1]), text.split(":")[-1] log.info(f"Want to connect to {ip}, {port}") try: await self.rpc_client.open_connection(ip, int(port)) except BaseException: # TODO: catch right exception self.error_msg.text = f"Failed to connect to {ip}:{port}" async def get_latest_blocks( self, heads: List[SmallHeaderBlock]) -> List[SmallHeaderBlock]: added_blocks: List[SmallHeaderBlock] = [] while len(added_blocks) < self.num_blocks and len(heads) > 0: heads = sorted(heads, key=lambda b: b.height, reverse=True) max_block = heads[0] if max_block not in added_blocks: added_blocks.append(max_block) heads.remove(max_block) prev: Optional[ SmallHeaderBlock] = await self.rpc_client.get_header( max_block.prev_header_hash) if prev is not None: heads.append(prev) return added_blocks async def draw_home(self): connections: List[Dict] = [c for c in self.connections] if set([con["node_id"] for con in connections]) != self.displayed_cons: new_con_rows = [] for con in connections: con_str = ( f"{NodeType(con['type']).name} {con['peer_host']} {con['peer_port']}/{con['peer_server_port']}" f" {con['node_id'].hex()[:10]}...") con_label = Label(text=con_str) def disconnect(c): async def inner(): await self.rpc_client.close_connection(c["node_id"]) self.layout.focus(self.quit_button) return inner disconnect_button = Button("Disconnect", handler=self.async_to_sync( disconnect(con))) row = VSplit([con_label, disconnect_button]) new_con_rows.append(row) self.displayed_cons = set([con["node_id"] for con in connections]) self.con_rows = new_con_rows if len(self.con_rows) > 0: self.layout.focus(self.con_rows[0]) else: self.layout.focus(self.quit_button) if len(self.con_rows): new_con_rows = HSplit(self.con_rows) else: new_con_rows = Window(width=D(), height=0) if self.sync_mode: if self.max_height >= 0: self.syncing.text = f"Syncing up to {self.max_height}" else: self.syncing.text = f"Syncing" else: self.syncing.text = "Not syncing" total_iters = self.lca_block.challenge.total_iters new_block_labels = [] for i, b in enumerate(self.latest_blocks): self.latest_blocks_labels[i].text = ( f"{b.height}:{b.header_hash}" f" {'LCA' if b.header_hash == self.lca_block.header_hash else ''}" f" {'TIP' if b.header_hash in [h.header_hash for h in self.tips] else ''}" ) self.latest_blocks_labels[i].handler = self.change_route_handler( f"block/{b.header_hash}") new_block_labels.append(self.latest_blocks_labels[i]) top_block_pools_labels = self.top_block_pools_labels if len(self.top_winners) > 0: new_top_block_pools_labels = [] for i, (winnings, pk) in enumerate(self.top_winners): self.top_block_pools_labels[ i].text = f"Public key {pk.hex()}: {winnings/1000000000000} chias." new_top_block_pools_labels.append( self.top_block_pools_labels[i]) top_block_pools_labels = new_top_block_pools_labels our_pools_labels = self.our_pools_labels if len(self.our_winners) > 0: new_our_pools_labels = [] for i, (winnings, pk) in enumerate(self.our_winners): self.our_pools_labels[ i].text = f"Public key {pk.hex()}: {winnings/(1000000000000)} chias." new_our_pools_labels.append(self.our_pools_labels[i]) our_pools_labels = new_our_pools_labels self.lca_label.text = ( f"Current least common ancestor {self.lca_block.header_hash}" f" height {self.lca_block.height}") self.current_heads_label.text = "Heights of tips: " + str( [h.height for h in self.tips]) self.difficulty_label.text = f"Current difficulty: {self.difficulty}" self.ips_label.text = f"Current VDF iterations per second: {self.ips}" self.total_iters_label.text = f"Total iterations since genesis: {total_iters}" try: if not self.focused: self.layout.focus(self.close_ui_button) self.focused = True except ValueError: # Not yet in layout pass return HSplit( [ self.syncing, self.lca_label, self.current_heads_label, self.difficulty_label, self.ips_label, self.total_iters_label, Window(height=1, char="-", style="class:line"), self.connections_msg, new_con_rows, Window(height=1, char="-", style="class:line"), self.add_connection_msg, self.add_connection_field, Window(height=1, char="-", style="class:line"), self.latest_blocks_msg, *new_block_labels, Window(height=1, char="-", style="class:line"), self.search_block_msg, self.search_block_field, Window(height=1, char="-", style="class:line"), self.top_block_pools_msg, *top_block_pools_labels, Window(height=1, char="-", style="class:line"), self.our_pools_msg, *our_pools_labels, Window(height=1, char="-", style="class:line"), self.close_ui_button, self.quit_button, self.error_msg, ], width=D(), height=D(), ) async def draw_block(self): block_hash: str = self.route.split("block/")[1] if self.block is None or self.block.header_hash != bytes32( bytes.fromhex(block_hash)): self.block: Optional[FullBlock] = await self.rpc_client.get_block( bytes32(bytes.fromhex(block_hash))) if self.block is not None: self.block_msg.text = f"Block {str(self.block.header_hash)}" if self.block_label.text != str(self.block): self.block_label.text = str(self.block) else: self.block_label.text = f"Block hash {block_hash} not found" try: if not self.focused: self.layout.focus(self.back_button) self.focused = True except ValueError: # Not yet in layout pass return HSplit([self.block_msg, self.block_label, self.back_button], width=D(), height=D()) async def update_ui(self): try: while not self.closed: if self.data_initialized: if self.route.startswith("home/"): self.content.body = await self.draw_home() elif self.route.startswith("block/"): self.content.body = await self.draw_block() if self.app and not self.app.invalidated: self.app.invalidate() await asyncio.sleep(0.5) except Exception as e: log.error(f"Exception in UI update_ui {type(e)}: {e}") raise e async def update_data(self): self.data_initialized = False counter = 0 try: while not self.closed: try: blockchain_state = await self.rpc_client.get_blockchain_state( ) self.lca_block = blockchain_state["lca"] self.tips = blockchain_state["tips"] self.difficulty = blockchain_state["difficulty"] self.ips = blockchain_state["ips"] self.sync_mode = blockchain_state["sync_mode"] self.connections = await self.rpc_client.get_connections() if self.sync_mode: max_block = await self.rpc_client.get_heaviest_block_seen( ) self.max_height = max_block.height self.latest_blocks = await self.get_latest_blocks(self.tips ) self.data_initialized = True if counter % 20 == 0: # Only request balances periodically, since it's an expensive operation coin_balances: Dict[ bytes, uint64] = await self.rpc_client.get_pool_balances( ) self.top_winners = sorted( [(rewards, key) for key, rewards in coin_balances.items()], reverse=True, )[:self.num_top_block_pools] self.our_winners = [ (coin_balances[bytes(pk)], bytes(pk)) if bytes(pk) in coin_balances else (0, bytes(pk)) for pk in self.pool_pks ] counter += 1 await asyncio.sleep(5) except ( aiohttp.client_exceptions.ClientConnectorError, aiohttp.client_exceptions.ServerConnectionError, ) as e: log.warning( f"Could not connect to full node. Is it running? {e}") await asyncio.sleep(5) except Exception as e: log.error(f"Exception in UI update_data {type(e)}: {e}") raise e async def await_closed(self): await self.update_ui_task await self.update_data_task
class UI(Client): def __init__(self, username: str, password: str): super().__init__(username, password, handle_data=DataFormat.ANSI) self.commands = [] self.output_buffer = Buffer_() self.cursor_pos = 0 self.chat_buffer = Buffer_() self.output = BufferControl(self.output_buffer, input_processors=[FormatText()], include_default_input_processors=True) self.chat = BufferControl(self.chat_buffer, input_processors=[FormatText()], include_default_input_processors=True) self.hide_ip = "--hide-ip" in sys.argv self.suggest = AutoSuggestFromLogs([ CommandSuggest(), ]) self.input = TextArea(height=1, prompt=" >> ", multiline=False, wrap_lines=False, accept_handler=self.accept, auto_suggest=self.suggest, dont_extend_width=True) self.host_ip = FormattedTextControl(ANSI("")) self.chat_float = Float(Frame(Window(self.chat, wrap_lines=True)), right=1, top=0, width=40, height=12, hide_when_covering_content=True) self.text = "" self.chat_text = "" def set_frame_size(fn): def inner(*args): size = self.app.output.get_size() self.chat_float.width = size.columns // 3 self.chat_float.height = size.rows // 2 return fn(*args) return inner self.out_window = Window(self.output, wrap_lines=True) kb = KeyBindings() @kb.add('c-c') @kb.add('c-q') def _(_): self.app.exit() self._loop = False self.run_again = False @kb.add('c-i', filter=has_focus(self.input)) def __(_): fut = self.suggest.get_suggestion_future(self.input.buffer, self.input.document) text = self.input.text def set_input(fut_2): res = fut_2.result() if res is not None: self.input.text = text + res.text self.input.document = Document(self.input.text, cursor_position=len( self.input.text)) fut.add_done_callback(set_input) @kb.add(Keys.ScrollUp) def sup(_): self.output_buffer.cursor_up(1) self.out_window._scroll_up() # pylint: disable=protected-access @kb.add(Keys.ScrollDown) def sdown(_): self.output_buffer.cursor_down(1) self.out_window._scroll_down() # pylint: disable=protected-access self.app = Application( layout=Layout( container=HSplit([ Frame( FloatContainer(self.out_window, floats=[self.chat_float])), Frame( VSplit([ self.input, Window(self.host_ip, align=WindowAlign.RIGHT, dont_extend_width=True) ])) ]), focused_element=self.input, ), full_screen=True, mouse_support=True, enable_page_navigation_bindings=True, key_bindings=merge_key_bindings([kb]), paste_mode=True, ) self.app._on_resize = set_frame_size(self.app._on_resize) # pylint: disable=protected-access self.run_again = True self.loop = get_event_loop() self._loop = False self.own_pass = "" self.own_ip = "" self.current_ip = "" # patch_stdout() @classmethod def create(cls) -> 'UI': user = prompt("Username: "******"Password: "******"{Fore.BLUE}[BROADCAST]{Fore.RESET} {converted_color_codes(e.msg)}" ) else: self.set_output(converted_color_codes(e.msg)) def event_error(self, e: ErrorEvent): self.err(e.error) @staticmethod def sanitize(arg: str) -> ANSI: return to_formatted_text(ANSI(arg)) def err(self, message: str): self.set_output( f"{Fore.RED}{converted_color_codes(message)}{Fore.RESET}") def set_chat_output(self, text: str): new_text = (self.chat_text + "\n" + text).strip() self.chat_buffer.set_text(new_text) self.chat_text = new_text.replace("\t", " ") def set_output(self, text: str): new_text = (self.text + "\n" + text).strip() self.output_buffer.set_text(new_text) self.text = new_text.replace("\t", " ") self.cursor_pos = len(self.text) def clear_chat(self): self.chat_buffer.set_text("") self.chat_text = "" def clear(self): self.output_buffer.set_text("") self.text = "" def accept(self, _): if self.input.text.strip(): self.set_output(f"\n>> {self.input.text}") self.commands.append(self.input.text) self.suggest.last_command(self.input.text) self.input.text = "" async def get_host_data(self): data = await self.command("pass see -l") self.own_pass = data.msg sys_info = await self.command('specs -l') self.current_ip = self.own_ip = re.search( r"(?P<ip>\d{1,3}(\.\d{1,3}){3,4})", sys_info.msg).group("ip") def launch(self): colorama.init() use_asyncio_event_loop() patch_stdout() while self.run_again: self.run_again = False self._loop = True self.loop.create_task(self.start()) try: self.loop.run_until_complete( self.app.run_async().to_asyncio_future()) except KeyboardInterrupt: if self.current_ip != self.own_ip: self.loop.run_until_complete(self.command("dc")) async def event_ready(self): o_text = self.output_buffer.text await self.get_host_data() text = (( f"{Fore.YELLOW}{self.own_ip} - {self.own_pass}{Fore.RESET} " if self.own_ip == self.current_ip else f"{Fore.YELLOW}{self.current_ip} / {self.own_ip} - {self.own_pass}{Fore.RESET} " ) if not self.hide_ip else "") self.host_ip.text = self.sanitize(text) self.clear() self.set_output(o_text) macros = MacroHolder() while self._loop: # TODO: Update these when they change macros.macros["self"] = self.own_ip macros.macros["pass"] = self.own_pass while not self.commands: await asyncio.sleep(0.1) try: line = self.commands.pop(0).strip() line = macros.parse(line) command, *args = line.split() if command == "chats": if len(args) == 1 and args[0] == "clear": self.clear_chat() else: await self.command(" ".join([command, *args])) elif command == "macro": if args and args[0] in ("add", "remove", "list"): sub = args.pop(0) if sub == "add": if len(args) >= 2: macros += args[0], " ".join(args[1:]) else: self.set_output( "Usage: macro add <name> <value>") elif sub == "remove": if args: macros -= args[0] else: self.set_output("Usage: macro remove <name>") else: self.set_output("Macros:") for key in sorted(list(macros)): self.set_output(f"${key} -> {macros[key]}") else: self.set_output("Usage: macro [add/remove/list] ...") elif command == "clear": self.clear() elif command == "quit": self._loop = False self.run_again = False self.stop() self.app.exit() else: await self.command(" ".join([command, *args])) except Exception as e: # pylint: disable=broad-except self.err(repr(e))
class Client: def __init__(self, url: str = "127.0.0.1", port: int = "9999"): self.communicator = Communicator(url, port) self.CMDIN = CommandInput(title="DLNest Command Line(F1)", onAccept=self.onCommandAccept) self.w1 = self.CMDIN.getWindow() self.DLOutput = ResultsOutput(routineTask=self.routineTaskDLOutput, title="DLNest Output (F2)", style="class:dlnest_output") self.w2 = self.DLOutput.getWindow() self.ANOutput = AnalyzeOutput(routineTask=self.routineTaskANOutput, title="Analyzer Output (F3)", style="class:analyzer_output") self.w3 = self.ANOutput.getWindow() self.analyzeTaskID = "" self.TaskInfo = TaskInfoShower(routineTask=self.routineTaskInfo, title="Tasks (F4)") self.w4 = self.TaskInfo.getWindow() self.DevicesInfo = DevicesInfoShower( routineTask=self.routineTaskDevices, title="Devices (F5)") self.w5 = self.DevicesInfo.getWindow() self.container_fat = HSplit( [self.w1, VSplit([self.w2, self.w3]), VSplit([self.w4, self.w5])]) self.container_tall = HSplit( [self.w1, self.w2, self.w3, self.w4, self.w5]) self.kb = KeyBindings() @self.kb.add('c-c') def exit_(event): event.app.exit() @self.kb.add('f1') def focus1(event): event.app.layout.focus(self.w1) @self.kb.add('f2') def focus2(event): event.app.layout.focus(self.w2) @self.kb.add('f3') def focus3(event): event.app.layout.focus(self.w3) @self.kb.add('f4') def focus4(event): event.app.layout.focus(self.w4) @self.kb.add('f5') def focus5(event): event.app.layout.focus(self.w5) self.style = Style.from_dict({ "frame.border": "fg:#ffb6c1", "frame.title": "fg:#1ef0ff", "command_frame": "bg:#008b8b", "dlnest_output": "bg:#451a4a", "analyzer_output": "bg:#451a4a", "analyzer_info_label": "bg:#da70d6", "analyzer_info_text1": "bg:#3f3f00", "analyzer_info_text2": "bg:#ff00ff", "running_task_status": "bg:#a01010 bold", "running_task_id": "bg:#303030", "running_task_gpu": "bg:#556b2f", "running_task_des": "bg:#c71585", "running_task_time": "bg:#2e3b37", "pending_task_status": "bg:#1010a0 bold", "pending_task_id": "bg:#303030", "pending_task_gpu": "bg:#556b2f", "pending_task_des": "bg:#c71585", "pending_task_time": "bg:#2e3b37", "suspend_task_status": "bg:#10a010 bold", "suspend_task_id": "bg:#303030", "suspend_task_gpu": "bg:#556b2f", "suspend_task_des": "bg:#c71585", "suspend_task_time": "bg:#2e3b37", "task_info_shower": "bg:#008bc0", "devices_info_shower": "bg:#008bc0", "devices_id": "bg:#303030", "devices_status_valid": "bg:#3cb371 bold", "devices_status_break": "bg:#a01010 bold", "devices_free_memory": "bg:#556b2f", "devices_tasks": "bg:#c71585" }) self.layout = Layout(self.container_fat, focused_element=self.w1) self.app = Application(key_bindings=self.kb, layout=self.layout, full_screen=True, style=self.style) self.app._on_resize = self.on_resize def on_resize(self): cols, rows = os.get_terminal_size(0) focused_element = self.layout.current_window if cols >= 2 * rows: # fat self.app.layout = Layout(self.container_fat, focused_element=focused_element) else: # tall self.app.layout = Layout(self.container_tall, focused_element=focused_element) self.app.renderer.erase(leave_alternate_screen=False) self.app._request_absolute_cursor_position() self.app._redraw() def getApp(self): return self.app def onCommandAccept(self, s: str): commandWordList = s.split(" ") while "" in commandWordList: commandWordList.remove("") if commandWordList[0] == "watch": self.analyzeTaskID = commandWordList[1] elif commandWordList[0] == "withdraw": self.analyzeTaskID = "" if commandWordList[0] == "runExp": if len(commandWordList) != 3: if self.analyzeTaskID != "": commandWordList = [ commandWordList[0], self.analyzeTaskID, commandWordList[1] ] else: return ret = self.communicator.giveACommand(commandWordList) if commandWordList[0] == "del": if ret["status"] == "success" and commandWordList[ 1] == self.analyzeTaskID: self.analyzeTaskID = "" if "exit" in ret: self.app.exit() def routineTaskDLOutput(self, obj): #for buffer fresh if not hasattr(obj, "_count_"): obj._count_ = 0 outStyledDict = self.communicator.giveACommand(["showDL", "-s"]) outPlainDict = self.communicator.giveACommand(["showDL"]) if "text" in outStyledDict and "text" in outPlainDict: try: obj.lexer.styled_text = outStyledDict["text"] obj.shower.text = outPlainDict["text"] except Exception as e: pass def routineTaskANOutput(self, obj): #for buffer fresh if not hasattr(obj, "_count_"): obj._count_ = 0 if self.analyzeTaskID == "": obj.lexer.styled_text = [] obj.shower.text = "" obj.infoText.text = [("", "No valid analyzer task is running")] obj.infoWindow.width = 33 return outStyledDict = self.communicator.giveACommand( ["showAN", "-t", self.analyzeTaskID, "-s"]) outPlainDict = self.communicator.giveACommand( ["showAN", "-t", self.analyzeTaskID]) if "text" in outStyledDict and "text" in outPlainDict: try: obj.lexer.styled_text = outStyledDict["text"] obj.shower.text = outPlainDict["text"] obj.infoText.text = [("class:analyzer_info_text1", self.analyzeTaskID)] obj.infoWindow.width = len(self.analyzeTaskID) except Exception as e: pass else: self.analyzeTaskID = "" def routineTaskInfo(self, obj): # for buffer fresh if not hasattr(obj, "_count_"): obj._count_ = 0 r = self.communicator.giveACommand(["showTask"]) if r["status"] != "success": obj.lexer.taskInfo = [] obj.shower.text = obj.lexer.get_text() return taskInfo = r["info"] try: obj.lexer.taskInfo = taskInfo obj.shower.text = obj.lexer.get_text() except Exception as e: pass def routineTaskDevices(self, obj): # for buffer fresh if not hasattr(obj, "_count_"): obj._count_ = 0 r = self.communicator.giveACommand(["showDevice"]) if r["status"] != "success": obj.lexer.devicesInfo = [] obj.shower.text = obj.lexer.get_text() return obj.lexer.devicesInfo = r["info"] try: obj.shower.text = obj.lexer.get_text() except Exception as e: pass
class PybreakGui(Bdb): def __init__(self): super().__init__() self.paused = True self.app_thread = threading.Thread(target=self.start_gui, args=(asyncio.get_event_loop(),)) self.frame_history = FrameHistory() self.search_toolbar = SearchToolbar( text_if_not_searching=[("class:not-searching", "Press '/' to start searching.")] ) def get_view_file(): if len(self.frame_history.history) > 0: return self.frame_history.hist_frame.filename return ".py" self.text_area = TextArea( lexer=DynamicLexer( lambda: PygmentsLexer.from_filename( get_view_file(), sync_from_start=False ) ), search_field=self.search_toolbar, scrollbar=True, line_numbers=True, ) self.container = HSplit( [ self.text_area, self.search_toolbar, ] ) kb = KeyBindings() @kb.add("c-q") def _(event: KeyPressEvent): self.paused = False self._quit() @kb.add("c-n") def _(event): self.set_next(self.frame_history.exec_frame.raw_frame) self.paused = False # allow another frame to be processed self.app = Application( full_screen=True, layout=Layout(container=self.container), key_bindings=kb, ) self.app.loop = asyncio.get_event_loop() def start_gui(self, loop): asyncio.set_event_loop(loop) self.app.run() self.text_area.buffer.insert_text("HELLO WORLD") def start(self, frame): self.app_thread.start() super().set_trace(frame) def _quit(self): sys.settrace(None) self.quitting = True self.app.exit() def user_call(self, frame: types.FrameType, argument_list): # if self.stop_here(frame): # self.frame_history.append(frame) pass def user_line(self, frame: types.FrameType): """ This method is called from dispatch_line() when either stop_here() or break_here() yields True. i.e. when we stop OR break at this line. * stop_here() yields true if the frame lies below the frame where debugging started on the call stack. i.e. it will be called for every line after we start debugging. * break_here() yields true only if there's a breakpoint for this line """ self.text_area.buffer.insert_text(f"FRAME = {frame.f_code.co_filename}:{frame.f_lineno}") if self.stop_here(frame): self.text_area.buffer.insert_text(str(frame.f_code.co_filename) + str(frame.f_lineno) + "\n") self.frame_history.append(frame) while self.paused: time.sleep(.1) self.paused = True
class View: def __init__(self, manager: mp.Manager, shared_state: dict, rpc_channel: mp.Queue): self.manager = manager self.shared_state = shared_state self.rpc_channel = rpc_channel self.fileman = Filemanager(self) self.fileman_visible = True self.toolbar = Toolbar(self) self.views = OrderedDict() # type: Dict[str, SimpleView] self.app = Application( full_screen=True, mouse_support=True, color_depth=ColorDepth.DEPTH_24_BIT, clipboard=InMemoryClipboard(), enable_page_navigation_bindings=False, # key_bindings=get_filemanager_kb() layout=Layout( container=HSplit([ DynamicContainer(self._get_children), DynamicContainer(lambda: self.toolbar) ]), # focused_element=(self.current_view or self.fileman).input_field, ), ) def _get_children(self): children = ([self.fileman] if self.fileman_visible else []) \ + list(self.views.values()) return VSplit( children=children, padding_char=Border.VERTICAL, padding=1, padding_style='#ffff00' ) if len(children) > 0 else Window() # VSplit([]) will raise Exception @property def current_view(self) -> SimpleView: return self.views.get(self.shared_state['focused_view']) def set_focus(self, view_id: str) -> None: self.shared_state['focused_view'] = view_id # When creating a new_view then set_focus will be called immediately after # This will create a race-condition between app.focus("LOADING...") and the # "LOADING..." component being replaced by the actually content (SimpleView # starts a thread that listen on the RPC channel, which often takes "a while" # so wait for the app to be ready (is_dirty != None) before calling set_focus: threading.Thread(target=self._set_focus, args=(view_id, )).start() def _set_focus(self, view_id: str) -> None: # Break if multiple threads are competing for focus: while self.shared_state['focused_view'] == view_id: if self.current_view.is_dirty is None: sleep(.1) else: self.app.layout.focus(self.current_view.input_field) break def new_view(self, file_path: str = None): channel = self.manager.Queue() self.rpc_channel.edit_request('new_view', {} if file_path is None else {'file_path': file_path}, channel) # Wait for 'view-id-X' identifier: view_id = channel.get() assert view_id not in self.shared_state['view_channels'], "Duplicate view_id: {} ({})".format(view_id, self.shared_state) self.shared_state['view_channels'][view_id] = channel self.views[view_id] = SimpleView(file_path, channel, view_id, self) self.set_focus(view_id) def close_view(self, view_id: str): self.rpc_channel.notify('close_view', {'view_id': view_id}) self.shared_state['view_channels'][view_id].put(('kill', {})) self.views[view_id].thread.join() del self.shared_state['view_channels'][view_id] del self.views[view_id] if view_id == self.shared_state['focused_view']: self.shared_state['focused_view'] = None try: new_focused_view = list(self.views)[0] self.set_focus(new_focused_view) except IndexError: if self.fileman_visible: self.app.layout.focus(self.fileman.input_field) else: logging.debug("[View] Calling app.exit()") self.app.exit()
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