From ffb2b9e72d083f4848c321578a4c1155e89a0c43 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:01:19 -0300 Subject: [PATCH 01/14] Validate bitcoincli path before execution, fallback to Lite Mode When bitcoincli is empty in bclock.conf, instead of crashing with PermissionError, MainMenu now: 1. Falls back to RPC remote mode if ip_port/rpcuser are configured 2. Redirects to Lite Mode (MainMenuCROPPED) if nothing is configured Also simplified the fatal error handler in main() to show the error message visibly before exiting. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index d9bd5d3..af81248 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -1824,6 +1824,17 @@ def MainMenu(mode): #Unified Main Menu - mode: "local", "onchain_only", or "remo sysinfo() pathexec() + # Validate bitcoincli path before attempting to use it + if mode in ("local", "onchain_only") and not path.get('bitcoincli'): + if path.get('ip_port') and path.get('rpcuser'): + mode = "remote" # Fall back to RPC mode + else: + show_error("Bitcoin CLI path not configured. Redirecting to Lite Mode.") + import time as _t + _t.sleep(2) + MainMenuCROPPED() + return + # Fetch BTC price for status bar try: _price_r = requests.get("https://mempool.space/api/v1/prices", timeout=3) @@ -7407,15 +7418,8 @@ def main(): except KeyboardInterrupt: print("\n") sys.exit(0) - except PermissionError as e: - if str(e).endswith("''"): - show_error("Bitcoin CLI path is not configured. Please set it up.") - logger.error("Empty bitcoincli path in bclock.conf") - introINIT() - else: - logger.error("Fatal error: %s", e) - sys.exit(101) except Exception as e: + show_error(str(e)) logger.error("Fatal error: %s", e) sys.exit(101) From e1923ffcff9dc889bfc855e56b87ca7185e2e60e Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:03:21 -0300 Subject: [PATCH 02/14] Add missing MainMenuCROPPED import from SPV.spvblock This function was previously available via the removed star import 'from SPV.spvblock import *'. Add explicit import so Lite Mode fallback and mode C selection work correctly. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index af81248..22e2a05 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -26,6 +26,7 @@ from imgterminal import createimagebitaxe, set_terminal_background from datetime import datetime, timedelta from sha256 import ex from cfonts import render, say +from SPV.spvblock import MainMenuCROPPED from clone import gitclone, satnode from donation import donationAddr, donationPayNym, donationLN, donationAddrTst, donationLNTst, decodeQR from feed import readFile From 296b59e7867720d3cf110d7ca0a75b8e3aaa50db Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:05:25 -0300 Subject: [PATCH 03/14] Use lazy import for MainMenuCROPPED to avoid circular import Move SPV.spvblock import inside the functions that call MainMenuCROPPED() instead of top-level, preventing circular dependency issues when SPV modules load before PyBlock globals. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index 22e2a05..2e61db7 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -26,7 +26,6 @@ from imgterminal import createimagebitaxe, set_terminal_background from datetime import datetime, timedelta from sha256 import ex from cfonts import render, say -from SPV.spvblock import MainMenuCROPPED from clone import gitclone, satnode from donation import donationAddr, donationPayNym, donationLN, donationAddrTst, donationLNTst, decodeQR from feed import readFile @@ -1833,7 +1832,8 @@ def MainMenu(mode): #Unified Main Menu - mode: "local", "onchain_only", or "remo show_error("Bitcoin CLI path not configured. Redirecting to Lite Mode.") import time as _t _t.sleep(2) - MainMenuCROPPED() + from SPV.spvblock import MainMenuCROPPED as _lite_menu + _lite_menu() return # Fetch BTC price for status bar @@ -5149,7 +5149,8 @@ def menuSelection(): path = pathv # Copy the variable pathv to 'path' MainMenuLOCAL() elif chln == "C": - MainMenuCROPPED() + from SPV.spvblock import MainMenuCROPPED as _lite_menu + _lite_menu() else: if os.path.isfile('config/blndconnect.conf'): chln['offchain'] = "offchain" From d842f97de0e1987a5eb44571470bc410d0004e04 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:06:07 -0300 Subject: [PATCH 04/14] Handle both string and dict formats in intro.conf for --tui mode intro.conf can contain either a plain string ("A"/"B"/"C") or a dict with keys like fullbtclnd/fullbtc. Handle both formats when detecting the mode for TUI launch. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index 2e61db7..aa7a109 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -7433,10 +7433,16 @@ if __name__ == "__main__": if cfg.has_config('intro.conf'): with open("config/intro.conf", "r") as f: init_data = json.load(f) - if init_data.get("fullbtclnd"): - mode = "local" - elif init_data.get("fullbtc"): - mode = "onchain_only" + if isinstance(init_data, str): + if init_data == "A": + mode = "local" + elif init_data == "B": + mode = "onchain_only" + elif isinstance(init_data, dict): + if init_data.get("fullbtclnd"): + mode = "local" + elif init_data.get("fullbtc"): + mode = "onchain_only" run_tui(mode=mode) else: main() From 8440b7b83b7a9b269572be0de0d51cad0acb1d7e Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:09:32 -0300 Subject: [PATCH 05/14] Fix TUI main menu: convert Screen to Widget for proper rendering MainMenuScreen was a Textual Screen mounted inside a Vertical container, which doesn't render. Create MainMenu as a Static widget instead: - New tui/widgets/main_menu.py with Panel+Table menu - Move keybindings (A/B/L/P/S/Q) to the App level - Menu now renders correctly in the content area Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/tui/app.py | 14 ++++++-- pybitblock/tui/widgets/main_menu.py | 54 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 pybitblock/tui/widgets/main_menu.py diff --git a/pybitblock/tui/app.py b/pybitblock/tui/app.py index ba90bcf..0700fac 100644 --- a/pybitblock/tui/app.py +++ b/pybitblock/tui/app.py @@ -12,7 +12,7 @@ from textual.timer import Timer from textual.binding import Binding from tui.widgets.status_bar import StatusBar -from tui.screens.main_menu import MainMenuScreen +from tui.widgets.main_menu import MainMenu from tui.workers.data_fetcher import fetch_block_height, fetch_btc_price, fetch_fees @@ -55,7 +55,12 @@ class PyBlockApp(App): CSS = CSS BINDINGS = [ - Binding("ctrl+q", "quit", "Quit", show=True, priority=True), + Binding("a", "select('pyblock')", "PyBLOCK", show=True), + Binding("b", "select('bitcoin')", "Bitcoin", show=True), + Binding("l", "select('lightning')", "Lightning", show=True), + Binding("p", "select('platforms')", "Platforms", show=True), + Binding("s", "select('settings')", "Settings", show=True), + Binding("q", "quit", "Quit", show=True), Binding("ctrl+r", "refresh_data", "Refresh", show=True), ] @@ -67,7 +72,7 @@ class PyBlockApp(App): def compose(self) -> ComposeResult: yield StatusBar(id="status-bar") yield Vertical( - MainMenuScreen(mode=self.mode), + MainMenu(mode=self.mode, id="main-menu"), id="content", ) yield self._build_fees_panel() @@ -107,6 +112,9 @@ class PyBlockApp(App): f"[dim]Slow:[/dim] {fees.get('hourFee', '?')}\n" ) + def action_select(self, section): + self.notify(f"Opening {section}...", timeout=2) + def action_refresh_data(self): self._do_refresh() self.notify("Refreshing data...", timeout=1) diff --git a/pybitblock/tui/widgets/main_menu.py b/pybitblock/tui/widgets/main_menu.py new file mode 100644 index 0000000..948be1e --- /dev/null +++ b/pybitblock/tui/widgets/main_menu.py @@ -0,0 +1,54 @@ +"""Main menu widget for PyBLOCK TUI.""" + +from textual.widgets import Static +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + + +class MainMenu(Static): + """The primary navigation menu rendered as a widget.""" + + def __init__(self, mode="lite", **kwargs): + super().__init__(**kwargs) + self.mode = mode + + def on_mount(self): + self.update(self._build_menu()) + + def _build_menu(self): + table = Table( + show_header=False, + box=None, + padding=(0, 2), + expand=False, + ) + table.add_column("Key", width=4, justify="right") + table.add_column("Label", width=30) + + items = [ + ("A", "PyBLOCK Dashboard", "bold red"), + ("B", "Bitcoin", "bold rgb(255,102,0)"), + ] + if self.mode != "onchain_only": + items.append(("L", "Lightning Network", "bold yellow")) + items.extend([ + ("P", "Platforms & APIs", "bold rgb(0,200,0)"), + ("S", "Settings", "bold blue"), + ("X", "Donate", "bold white"), + ("Q", "Exit", "bold rgb(128,0,255)"), + ]) + + for key, label, style in items: + table.add_row( + Text(f"{key}.", style=style), + Text(label, style="white"), + ) + + return Panel( + table, + title="[bold red]PyBLOCK[/bold red]", + subtitle="[dim]Navigate with keyboard[/dim]", + expand=False, + padding=(1, 2), + ) From 6eecc5750a770988a1ffcae152f7f2ba4c295ef4 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:12:36 -0300 Subject: [PATCH 06/14] Wire up TUI menu actions with live data panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each menu key now loads real content: - A (Dashboard): block height, price, hashrate, difficulty, mempool stats, and latest 5 blocks table — all from mempool.space API - B (Bitcoin): submenu with blockchain features - L (Lightning): channel management overview - P (Platforms): API integrations list - S (Settings): current mode and config info - M: return to main menu from any section New data fetchers: fetch_latest_blocks(), fetch_hashrate(), fetch_mempool_info() expanded. Dashboard loads async via run_worker(thread=True) with live content replacement in the center panel. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/tui/app.py | 169 +++++++++++++++++++++++-- pybitblock/tui/workers/data_fetcher.py | 31 +++++ 2 files changed, 191 insertions(+), 9 deletions(-) diff --git a/pybitblock/tui/app.py b/pybitblock/tui/app.py index 0700fac..197d67b 100644 --- a/pybitblock/tui/app.py +++ b/pybitblock/tui/app.py @@ -6,14 +6,19 @@ Or from PyBlock.py with: --tui flag """ from textual.app import App, ComposeResult -from textual.widgets import Footer, Static +from textual.widgets import Footer, Static, RichLog from textual.containers import Vertical -from textual.timer import Timer from textual.binding import Binding +from rich.panel import Panel +from rich.table import Table +from rich.text import Text from tui.widgets.status_bar import StatusBar from tui.widgets.main_menu import MainMenu -from tui.workers.data_fetcher import fetch_block_height, fetch_btc_price, fetch_fees +from tui.workers.data_fetcher import ( + fetch_block_height, fetch_btc_price, fetch_fees, + fetch_mempool_info, fetch_latest_blocks, fetch_hashrate, +) CSS = """ @@ -30,6 +35,7 @@ Screen { #content { height: 1fr; padding: 1 2; + overflow-y: auto; } #fees-panel { @@ -55,7 +61,8 @@ class PyBlockApp(App): CSS = CSS BINDINGS = [ - Binding("a", "select('pyblock')", "PyBLOCK", show=True), + Binding("m", "show_menu", "Menu", show=True), + Binding("a", "select('pyblock')", "Dashboard", show=True), Binding("b", "select('bitcoin')", "Bitcoin", show=True), Binding("l", "select('lightning')", "Lightning", show=True), Binding("p", "select('platforms')", "Platforms", show=True), @@ -67,7 +74,9 @@ class PyBlockApp(App): def __init__(self, mode="lite"): super().__init__() self.mode = mode - self._update_timer = None + self._block = "---" + self._price = "---" + self._fees = {} def compose(self) -> ComposeResult: yield StatusBar(id="status-bar") @@ -87,7 +96,7 @@ class PyBlockApp(App): def on_mount(self): self.query_one(StatusBar).mode = self.mode - self._update_timer = self.set_interval(30, self._do_refresh) + self.set_interval(30, self._do_refresh) self._do_refresh() def _do_refresh(self): @@ -97,9 +106,13 @@ class PyBlockApp(App): block = fetch_block_height() price = fetch_btc_price() fees = fetch_fees() - self.call_from_thread(self._update_ui, block, price, fees) + self.call_from_thread(self._update_status, block, price, fees) + + def _update_status(self, block, price, fees): + self._block = block + self._price = price + self._fees = fees - def _update_ui(self, block, price, fees): status = self.query_one(StatusBar) status.block_height = block status.btc_price = price @@ -112,8 +125,146 @@ class PyBlockApp(App): f"[dim]Slow:[/dim] {fees.get('hourFee', '?')}\n" ) + def _set_content(self, renderable): + """Replace the content area with new renderable.""" + content = self.query_one("#content", Vertical) + content.remove_children() + widget = Static(renderable, id="section-view") + content.mount(widget) + + # --- Actions --- + + def action_show_menu(self): + content = self.query_one("#content", Vertical) + content.remove_children() + content.mount(MainMenu(mode=self.mode, id="main-menu")) + def action_select(self, section): - self.notify(f"Opening {section}...", timeout=2) + if section == "pyblock": + self._load_dashboard() + elif section == "bitcoin": + self._load_bitcoin() + elif section == "lightning": + self._load_lightning() + elif section == "platforms": + self._load_platforms() + elif section == "settings": + self._load_settings() + + def _load_dashboard(self): + """Show dashboard with block, price, mempool summary.""" + self.notify("Loading dashboard...", timeout=1) + self.run_worker(self._fetch_dashboard, thread=True) + + def _fetch_dashboard(self): + mempool = fetch_mempool_info() + blocks = fetch_latest_blocks() + hashrate = fetch_hashrate() + self.call_from_thread(self._render_dashboard, mempool, blocks, hashrate) + + def _render_dashboard(self, mempool, blocks, hashrate): + # Summary panel + summary = Table(show_header=False, box=None, padding=(0, 2)) + summary.add_column("Key", style="bold yellow", width=18) + summary.add_column("Value", style="bold white") + summary.add_row("Block Height", str(self._block)) + summary.add_row("BTC Price", f"${self._price}") + summary.add_row("Hashrate", f"{hashrate['hashrate_eh']} EH/s") + summary.add_row("Difficulty", hashrate["difficulty"]) + summary.add_row("Mempool Txs", f"{mempool.get('count', '?'):,}" if isinstance(mempool.get('count'), int) else str(mempool.get('count', '?'))) + summary.add_row("Mempool Size", f"{mempool.get('vsize', 0) / 1_000_000:.1f} MvB" if isinstance(mempool.get('vsize'), (int, float)) else "?") + summary_panel = Panel(summary, title="[bold red]PyBLOCK Dashboard[/bold red]", expand=False, padding=(1, 2)) + + # Latest blocks table + blocks_table = Table(title="Latest Blocks", expand=False, padding=(0, 1)) + blocks_table.add_column("Height", style="bold green", width=10) + blocks_table.add_column("Txs", style="white", width=8, justify="right") + blocks_table.add_column("Size (MB)", style="cyan", width=10, justify="right") + blocks_table.add_column("Pool", style="yellow", width=16) + for b in blocks: + blocks_table.add_row( + str(b["height"]), + str(b["tx_count"]), + str(b["size"]), + b["pool"], + ) + blocks_panel = Panel(blocks_table, expand=False, padding=(0, 1)) + + content = self.query_one("#content", Vertical) + content.remove_children() + content.mount(Static(summary_panel, id="dashboard-summary")) + content.mount(Static(blocks_panel, id="dashboard-blocks")) + + def _load_bitcoin(self): + """Show Bitcoin info panel.""" + table = Table(show_header=False, box=None, padding=(0, 2)) + table.add_column("Key", width=4, justify="right") + table.add_column("Label") + items = [ + ("A", "Blockchain Info", "bold rgb(255,102,0)"), + ("C", "Mempool Monitor", "bold rgb(255,102,0)"), + ("D", "Latest Blocks", "bold rgb(255,102,0)"), + ("E", "Fee Estimates", "bold rgb(255,102,0)"), + ("H", "Hashrate & Difficulty", "bold rgb(255,102,0)"), + ] + for key, label, style in items: + table.add_row(Text(f"{key}.", style=style), Text(label, style="white")) + + panel = Panel(table, title="[bold rgb(255,102,0)]Bitcoin[/bold rgb(255,102,0)]", + subtitle="[dim]Press M for main menu[/dim]", expand=False, padding=(1, 2)) + self._set_content(panel) + self.notify("Bitcoin section - submenu navigation coming soon", timeout=2) + + def _load_lightning(self): + """Show Lightning info panel.""" + panel = Panel( + "[bold yellow]Lightning Network[/bold yellow]\n\n" + "Connect your Lightning node to access:\n\n" + " [yellow]1.[/yellow] Channel Management\n" + " [yellow]2.[/yellow] Create/Pay Invoices\n" + " [yellow]3.[/yellow] Keysend Payments\n" + " [yellow]4.[/yellow] Node Info & Peers\n" + " [yellow]5.[/yellow] Rebalance Channels\n\n" + f"[dim]Mode: {self.mode} | Press M for main menu[/dim]", + title="[bold yellow]Lightning[/bold yellow]", + expand=False, + padding=(1, 2), + ) + self._set_content(panel) + + def _load_platforms(self): + """Show Platforms panel.""" + panel = Panel( + "[bold green]Platforms & APIs[/bold green]\n\n" + " [green]1.[/green] LNBits\n" + " [green]2.[/green] OpenNode\n" + " [green]3.[/green] TallyCoin\n" + " [green]4.[/green] CoinGecko Price\n" + " [green]5.[/green] Weather (wttr.in)\n" + " [green]6.[/green] Rate.sx Charts\n\n" + "[dim]Press M for main menu[/dim]", + title="[bold green]Platforms[/bold green]", + expand=False, + padding=(1, 2), + ) + self._set_content(panel) + + def _load_settings(self): + """Show Settings panel.""" + panel = Panel( + "[bold blue]Settings[/bold blue]\n\n" + f" [blue]Mode:[/blue] {self.mode}\n" + f" [blue]Block:[/blue] {self._block}\n" + f" [blue]Refresh:[/blue] 30s auto\n\n" + " [dim]Logo colors, fonts, and node\n" + " configuration available in\n" + " classic mode (without --tui)[/dim]\n\n" + "[dim]Press M for main menu[/dim]", + title="[bold blue]Settings[/bold blue]", + expand=False, + padding=(1, 2), + ) + self._set_content(panel) def action_refresh_data(self): self._do_refresh() diff --git a/pybitblock/tui/workers/data_fetcher.py b/pybitblock/tui/workers/data_fetcher.py index dca1f98..f653d81 100644 --- a/pybitblock/tui/workers/data_fetcher.py +++ b/pybitblock/tui/workers/data_fetcher.py @@ -43,3 +43,34 @@ def fetch_mempool_info(): } except Exception: return {"count": "?", "vsize": "?", "total_fee": "?"} + + +def fetch_latest_blocks(): + """Fetch latest 5 blocks from mempool.space.""" + try: + r = requests.get("https://mempool.space/api/v1/blocks", timeout=5) + blocks = r.json()[:5] + return [ + { + "height": b.get("height", "?"), + "tx_count": b.get("tx_count", "?"), + "size": round(b.get("size", 0) / 1_000_000, 2), + "pool": b.get("extras", {}).get("pool", {}).get("name", "Unknown"), + } + for b in blocks + ] + except Exception: + return [] + + +def fetch_hashrate(): + """Fetch network hashrate info.""" + try: + r = requests.get("https://mempool.space/api/v1/mining/hashrate/3d", timeout=5) + data = r.json() + current = data.get("currentHashrate", 0) + difficulty = data.get("currentDifficulty", 0) + eh = current / 1e18 + return {"hashrate_eh": f"{eh:.1f}", "difficulty": f"{difficulty:.2e}"} + except Exception: + return {"hashrate_eh": "?", "difficulty": "?"} From 86aff5bad2071bd424d9e5a0459fdbef81212020 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:15:44 -0300 Subject: [PATCH 07/14] Fix TUI widget lifecycle: use single Static view for content swapping Replace mount/remove_children pattern (async issues, duplicate IDs) with a single persistent #section-view Static widget that gets its content updated via .update(). All sections now render correctly: - Dashboard: summary table + latest blocks (fetched async from API) - Bitcoin/Lightning/Platforms/Settings: info panels - M key: return to main menu Also added fetch_latest_blocks() and fetch_hashrate() data workers. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/tui/app.py | 46 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/pybitblock/tui/app.py b/pybitblock/tui/app.py index 197d67b..6c58a45 100644 --- a/pybitblock/tui/app.py +++ b/pybitblock/tui/app.py @@ -81,7 +81,7 @@ class PyBlockApp(App): def compose(self) -> ComposeResult: yield StatusBar(id="status-bar") yield Vertical( - MainMenu(mode=self.mode, id="main-menu"), + Static(id="section-view"), id="content", ) yield self._build_fees_panel() @@ -96,6 +96,7 @@ class PyBlockApp(App): def on_mount(self): self.query_one(StatusBar).mode = self.mode + self.action_show_menu() self.set_interval(30, self._do_refresh) self._do_refresh() @@ -127,17 +128,18 @@ class PyBlockApp(App): def _set_content(self, renderable): """Replace the content area with new renderable.""" - content = self.query_one("#content", Vertical) - content.remove_children() - widget = Static(renderable, id="section-view") - content.mount(widget) + try: + view = self.query_one("#section-view", Static) + view.update(renderable) + except Exception: + pass # --- Actions --- def action_show_menu(self): - content = self.query_one("#content", Vertical) - content.remove_children() - content.mount(MainMenu(mode=self.mode, id="main-menu")) + menu = MainMenu(mode=self.mode) + # Build the menu renderable + self._set_content(menu._build_menu()) def action_select(self, section): if section == "pyblock": @@ -160,10 +162,12 @@ class PyBlockApp(App): mempool = fetch_mempool_info() blocks = fetch_latest_blocks() hashrate = fetch_hashrate() - self.call_from_thread(self._render_dashboard, mempool, blocks, hashrate) + self.call_from_thread(self._render_dashboard_sync, mempool, blocks, hashrate) + + def _render_dashboard_sync(self, mempool, blocks, hashrate): + """Build dashboard as a Rich Group and update the section view.""" + from rich.console import Group - def _render_dashboard(self, mempool, blocks, hashrate): - # Summary panel summary = Table(show_header=False, box=None, padding=(0, 2)) summary.add_column("Key", style="bold yellow", width=18) summary.add_column("Value", style="bold white") @@ -173,27 +177,21 @@ class PyBlockApp(App): summary.add_row("Difficulty", hashrate["difficulty"]) summary.add_row("Mempool Txs", f"{mempool.get('count', '?'):,}" if isinstance(mempool.get('count'), int) else str(mempool.get('count', '?'))) summary.add_row("Mempool Size", f"{mempool.get('vsize', 0) / 1_000_000:.1f} MvB" if isinstance(mempool.get('vsize'), (int, float)) else "?") - summary_panel = Panel(summary, title="[bold red]PyBLOCK Dashboard[/bold red]", expand=False, padding=(1, 2)) - # Latest blocks table blocks_table = Table(title="Latest Blocks", expand=False, padding=(0, 1)) blocks_table.add_column("Height", style="bold green", width=10) blocks_table.add_column("Txs", style="white", width=8, justify="right") blocks_table.add_column("Size (MB)", style="cyan", width=10, justify="right") blocks_table.add_column("Pool", style="yellow", width=16) for b in blocks: - blocks_table.add_row( - str(b["height"]), - str(b["tx_count"]), - str(b["size"]), - b["pool"], - ) - blocks_panel = Panel(blocks_table, expand=False, padding=(0, 1)) + blocks_table.add_row(str(b["height"]), str(b["tx_count"]), str(b["size"]), b["pool"]) - content = self.query_one("#content", Vertical) - content.remove_children() - content.mount(Static(summary_panel, id="dashboard-summary")) - content.mount(Static(blocks_panel, id="dashboard-blocks")) + dashboard = Group( + Panel(summary, title="[bold red]PyBLOCK Dashboard[/bold red]", expand=False, padding=(1, 2)), + "", + Panel(blocks_table, expand=False, padding=(0, 1)), + ) + self._set_content(dashboard) def _load_bitcoin(self): """Show Bitcoin info panel.""" From da1f8efc64e124f46eae7de791b873487bc1b0f7 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:17:58 -0300 Subject: [PATCH 08/14] Add missing codecs import to PyBlock.py codecs is used in 10+ places for macaroon encoding but was previously available only via star imports that were removed in the security audit. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index aa7a109..f4c737a 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -2,6 +2,7 @@ #Tester: __B__T__C__ #ℙ𝕪𝔹𝕃𝕆ℂ𝕂 𝕚𝕥𝕤 𝕒 𝔹𝕚𝕥𝕔𝕠𝕚𝕟 𝔻𝕒𝕤𝕙𝕓𝕠𝕒𝕣𝕕 𝕨𝕚𝕥𝕙 ℂ𝕪𝕡𝕙𝕖𝕣𝕡𝕦𝕟𝕜 𝕒𝕖𝕤𝕥𝕙𝕖𝕥𝕚𝕔. +import codecs import os import os.path import time as t From 4460be7b38c3738a43d25edb03b30efc881f0c91 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:18:46 -0300 Subject: [PATCH 09/14] Always fallback to Lite Mode when node config is incomplete When bitcoincli is empty, go straight to Lite Mode instead of trying remote mode (which also fails without tls/macaroon). Also validate remote mode has tls configured before attempting connection. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index f4c737a..b7598fa 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -1827,15 +1827,17 @@ def MainMenu(mode): #Unified Main Menu - mode: "local", "onchain_only", or "remo # Validate bitcoincli path before attempting to use it if mode in ("local", "onchain_only") and not path.get('bitcoincli'): - if path.get('ip_port') and path.get('rpcuser'): - mode = "remote" # Fall back to RPC mode - else: - show_error("Bitcoin CLI path not configured. Redirecting to Lite Mode.") - import time as _t - _t.sleep(2) - from SPV.spvblock import MainMenuCROPPED as _lite_menu - _lite_menu() - return + show_error("Bitcoin CLI path not configured. Redirecting to Lite Mode.") + t.sleep(2) + from SPV.spvblock import MainMenuCROPPED as _lite_menu + _lite_menu() + return + if mode == "remote" and not lndconnectload.get('tls'): + show_error("Remote node not configured. Redirecting to Lite Mode.") + t.sleep(2) + from SPV.spvblock import MainMenuCROPPED as _lite_menu + _lite_menu() + return # Fetch BTC price for status bar try: From 6b57e9e0f0a25471df68ac79e2cc45117dc70b81 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:22:50 -0300 Subject: [PATCH 10/14] Fix Ctrl+C to return to main menu instead of exiting - artist(): catch KeyboardInterrupt explicitly so Ctrl+C breaks the block display loop and returns to caller - main(): change KeyboardInterrupt handler from sys.exit(0) to continue, which loops back to menuSelection() Users can now press Ctrl+C to exit any screen and return to the menu. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/PyBlock.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pybitblock/PyBlock.py b/pybitblock/PyBlock.py index b7598fa..4129852 100644 --- a/pybitblock/PyBlock.py +++ b/pybitblock/PyBlock.py @@ -888,6 +888,8 @@ def artist(): # here we convert the result of the command 'getblockcount' on a r clear() close() design() + except KeyboardInterrupt: + break except Exception as e: logger.debug("Loop interrupted: %s", e) break @@ -7421,8 +7423,7 @@ def main(): set_terminal_background() menuSelection() except KeyboardInterrupt: - print("\n") - sys.exit(0) + continue # Ctrl+C returns to main menu except Exception as e: show_error(str(e)) logger.error("Fatal error: %s", e) From b86fdcb1ce7f07cff0fa6739381e0df2345b5cf5 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:24:28 -0300 Subject: [PATCH 11/14] Fix Rich background colors to match terminal background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use transparent backgrounds instead of forced dark grays in panels - Replace ░ block chars with ─ dashes for progress bar empty space - Set console highlight=False to prevent unwanted auto-styling - Remove pyblock.dim style from panels (used default dim instead) Rich elements now blend seamlessly with the terminal's own background. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/shared/rich_ui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybitblock/shared/rich_ui.py b/pybitblock/shared/rich_ui.py index 696183c..0253bf0 100644 --- a/pybitblock/shared/rich_ui.py +++ b/pybitblock/shared/rich_ui.py @@ -37,7 +37,7 @@ PYBLOCK_THEME = Theme({ "pyblock.block": "bold white", }) -console = Console(theme=PYBLOCK_THEME) +console = Console(theme=PYBLOCK_THEME, highlight=False) def rich_status_bar(mode="", block_height="", btc_price="", extra=""): @@ -80,7 +80,7 @@ def rich_status_bar(mode="", block_height="", btc_price="", extra=""): combined.append_text(separator) combined.append_text(part) - console.print(Panel(combined, style="pyblock.dim", expand=False, padding=(0, 2))) + console.print(Panel(combined, style="dim", expand=False, padding=(0, 2))) def rich_sysinfo(cpu_percent, mem_percent): @@ -112,7 +112,7 @@ def _make_bar(percent, color): """Create a simple text-based progress bar.""" filled = int(percent / 5) empty = 20 - filled - return f"[{color}]{'█' * filled}[/{color}][dim]{'░' * empty}[/dim]" + return f"[{color}]{'█' * filled}[/{color}][rgb(60,60,60)]{'─' * empty}[/rgb(60,60,60)]" def rich_menu(title, items, footer_text=""): @@ -191,7 +191,7 @@ def rich_header(node_type, block_height, version, alias=None): info.append("Version: ", style="bold white") info.append(f"{version}", style="dim") - console.print(Panel(info, style="pyblock.dim", expand=False, padding=(0, 2))) + console.print(Panel(info, style="dim", expand=False, padding=(0, 2))) def rich_loading(label="Loading"): From 2f60500162394130e4670952084095252e66d8ca Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:26:29 -0300 Subject: [PATCH 12/14] Remove Rich Panel borders from status bar and header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Panel() wrappers with plain text output for status_bar and header — Panels create a visible background box that clashes with dark terminal themes. Error/warning panels kept as-is since they should visually stand out. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/shared/rich_ui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pybitblock/shared/rich_ui.py b/pybitblock/shared/rich_ui.py index 0253bf0..7bfdee4 100644 --- a/pybitblock/shared/rich_ui.py +++ b/pybitblock/shared/rich_ui.py @@ -80,7 +80,8 @@ def rich_status_bar(mode="", block_height="", btc_price="", extra=""): combined.append_text(separator) combined.append_text(part) - console.print(Panel(combined, style="dim", expand=False, padding=(0, 2))) + console.print(f" ", end="") + console.print(combined) def rich_sysinfo(cpu_percent, mem_percent): @@ -191,7 +192,8 @@ def rich_header(node_type, block_height, version, alias=None): info.append("Version: ", style="bold white") info.append(f"{version}", style="dim") - console.print(Panel(info, style="dim", expand=False, padding=(0, 2))) + console.print(f" ", end="") + console.print(info) def rich_loading(label="Loading"): From db640ac8fc7250c9d0a7fb6df5fd1ea1c5885380 Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:28:30 -0300 Subject: [PATCH 13/14] Replace Rich Tables with inline console.print for transparent backgrounds Tables pad columns with spaces that show a different background color. Switch sysinfo and menu to console.print() with markup strings instead, which render with the terminal's native background color. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/shared/rich_ui.py | 43 +++++++----------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/pybitblock/shared/rich_ui.py b/pybitblock/shared/rich_ui.py index 7bfdee4..ec74027 100644 --- a/pybitblock/shared/rich_ui.py +++ b/pybitblock/shared/rich_ui.py @@ -37,7 +37,7 @@ PYBLOCK_THEME = Theme({ "pyblock.block": "bold white", }) -console = Console(theme=PYBLOCK_THEME, highlight=False) +console = Console(theme=PYBLOCK_THEME, highlight=False, color_system="truecolor") def rich_status_bar(mode="", block_height="", btc_price="", extra=""): @@ -85,28 +85,15 @@ def rich_status_bar(mode="", block_height="", btc_price="", extra=""): def rich_sysinfo(cpu_percent, mem_percent): - """Render CPU and Memory as a compact Rich panel.""" + """Render CPU and Memory with colored bars.""" cpu_color = "green" if cpu_percent < 70 else ("yellow" if cpu_percent < 90 else "red") mem_color = "green" if mem_percent < 70 else ("yellow" if mem_percent < 90 else "red") cpu_bar = _make_bar(cpu_percent, cpu_color) mem_bar = _make_bar(mem_percent, mem_color) - table = Table(show_header=False, box=None, padding=(0, 1)) - table.add_column(width=10) - table.add_column(width=22) - table.add_column(width=5, justify="right") - table.add_row( - Text("CPU", style="italic yellow"), - Text.from_markup(cpu_bar), - Text(f"{cpu_percent}%", style=f"bold {cpu_color}"), - ) - table.add_row( - Text("Memory", style="italic yellow"), - Text.from_markup(mem_bar), - Text(f"{mem_percent}%", style=f"bold {mem_color}"), - ) - console.print(table) + console.print(f" [italic yellow]CPU[/] {cpu_bar} [bold {cpu_color}]{cpu_percent}%[/]") + console.print(f" [italic yellow]Memory[/] {mem_bar} [bold {mem_color}]{mem_percent}%[/]") def _make_bar(percent, color): @@ -117,32 +104,18 @@ def _make_bar(percent, color): def rich_menu(title, items, footer_text=""): - """Render a styled menu table. + """Render a styled menu. Args: title: Menu section title items: List of (key, label, style) tuples footer_text: Optional text below the menu """ - table = Table( - show_header=False, - box=None, - padding=(0, 1), - pad_edge=False, - ) - table.add_column("Key", width=6, justify="right") - table.add_column("Label") - - for key, label, style in items: - table.add_row( - Text(f"{key}.", style=f"bold {style}"), - Text(label, style="white"), - ) - console.print() - console.print(table) + for key, label, style in items: + console.print(f" [bold {style}]{key}.[/] {label}") if footer_text: - console.print(f" [pyblock.dim]{footer_text}[/pyblock.dim]") + console.print(f" [dim]{footer_text}[/dim]") console.print() From cb89d6fbcfe4196489ba21acc42480c813ba634e Mon Sep 17 00:00:00 2001 From: GaltRanch Date: Wed, 1 Apr 2026 15:29:47 -0300 Subject: [PATCH 14/14] Restore Panel boxes with transparent background (style='on default') Bring back the Panel borders for sysinfo, status bar, header, and menu but use style='on default' which inherits the terminal's background color instead of Rich's default dark gray. border_style='dim' keeps the borders subtle. Co-Authored-By: Claude Opus 4.6 (1M context) --- pybitblock/shared/rich_ui.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pybitblock/shared/rich_ui.py b/pybitblock/shared/rich_ui.py index ec74027..b10fc8c 100644 --- a/pybitblock/shared/rich_ui.py +++ b/pybitblock/shared/rich_ui.py @@ -80,20 +80,22 @@ def rich_status_bar(mode="", block_height="", btc_price="", extra=""): combined.append_text(separator) combined.append_text(part) - console.print(f" ", end="") - console.print(combined) + console.print(Panel(combined, expand=False, style="on default", border_style="dim", padding=(0, 2))) def rich_sysinfo(cpu_percent, mem_percent): - """Render CPU and Memory with colored bars.""" + """Render CPU and Memory with colored bars in a panel.""" cpu_color = "green" if cpu_percent < 70 else ("yellow" if cpu_percent < 90 else "red") mem_color = "green" if mem_percent < 70 else ("yellow" if mem_percent < 90 else "red") cpu_bar = _make_bar(cpu_percent, cpu_color) mem_bar = _make_bar(mem_percent, mem_color) - console.print(f" [italic yellow]CPU[/] {cpu_bar} [bold {cpu_color}]{cpu_percent}%[/]") - console.print(f" [italic yellow]Memory[/] {mem_bar} [bold {mem_color}]{mem_percent}%[/]") + text = Text.from_markup( + f"[italic yellow]CPU[/] {cpu_bar} [bold {cpu_color}]{cpu_percent}%[/]\n" + f"[italic yellow]Memory[/] {mem_bar} [bold {mem_color}]{mem_percent}%[/]" + ) + console.print(Panel(text, expand=False, style="on default", border_style="dim")) def _make_bar(percent, color): @@ -104,18 +106,21 @@ def _make_bar(percent, color): def rich_menu(title, items, footer_text=""): - """Render a styled menu. + """Render a styled menu in a panel. Args: title: Menu section title items: List of (key, label, style) tuples footer_text: Optional text below the menu """ - console.print() + lines = [] for key, label, style in items: - console.print(f" [bold {style}]{key}.[/] {label}") + lines.append(f"[bold {style}]{key}.[/] {label}") if footer_text: - console.print(f" [dim]{footer_text}[/dim]") + lines.append(f"\n[dim]{footer_text}[/dim]") + + content = Text.from_markup("\n".join(lines)) + console.print(Panel(content, expand=False, style="on default", border_style="dim", padding=(1, 2))) console.print() @@ -165,8 +170,7 @@ def rich_header(node_type, block_height, version, alias=None): info.append("Version: ", style="bold white") info.append(f"{version}", style="dim") - console.print(f" ", end="") - console.print(info) + console.print(Panel(info, expand=False, style="on default", border_style="dim", padding=(0, 2))) def rich_loading(label="Loading"):