From 445bd65b4a4fe4b995abd170ccd6cdfcce778aa8 Mon Sep 17 00:00:00 2001 From: zenaku Date: Fri, 5 Jun 2026 10:33:27 -0500 Subject: [PATCH] radical change --- cli/commands/profile.py | 3 +- cli/observers.py | 64 ++++++++++++++++-- cli/ui.py | 144 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 cli/ui.py diff --git a/cli/commands/profile.py b/cli/commands/profile.py index 4f7902a..b6fcf89 100644 --- a/cli/commands/profile.py +++ b/cli/commands/profile.py @@ -117,11 +117,10 @@ def handle(arguments, main_parser): main_parser.error('sing-box is not installed. Please run the installation script first.') except (InvalidSubscriptionError, MissingSubscriptionError) as exception: _handle_subscription_error(exception, profile, ignore, arguments, main_parser) - elif arguments.subcommand == 'disable': profile = ProfileController.get(arguments.id) if profile is not None: - ProfileController.disable(profile, ignore=ignore, profile_observer=profile_observer) + ProfileController.disable(profile, ignore=ignore, profile_observer=profile_observer, connection_observer=connection_observer) else: main_parser.error('the following argument should be a valid reference: --id/-i') diff --git a/cli/observers.py b/cli/observers.py index a03d151..983990a 100644 --- a/cli/observers.py +++ b/cli/observers.py @@ -4,6 +4,7 @@ from core.observers.ClientObserver import ClientObserver from core.observers.ConnectionObserver import ConnectionObserver from core.observers.InvoiceObserver import InvoiceObserver from core.observers.ProfileObserver import ProfileObserver +from cli.ui import Spinner, label_ok, label_error, label_warn, label_connected, label_disconnect, progress_bar from cli.helpers import sanitize_profile import pprint @@ -22,10 +23,63 @@ client_observer.subscribe('updating', lambda event: print('Updating client...')) client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r')) client_observer.subscribe('updated', lambda event: print('\n')) -connection_observer.subscribe('connecting', lambda event: print(f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...\n')) -connection_observer.subscribe('tor_bootstrapping', lambda event: print('Bootstrapping Tor...')) -connection_observer.subscribe('tor_bootstrap_progressing', lambda event: print(f'Current progress: {event.meta.get("progress"):.2f}%', flush=True, end='\r')) -connection_observer.subscribe('tor_bootstrapped', lambda event: print('\n')) +_spinner = None + +def _on_connecting(event): + global _spinner + attempt = event.subject.get("attempt_count", "?") + total = event.subject.get("maximum_number_of_attempts", "?") + _spinner = Spinner(f"Connecting... [{attempt}/{total}]") + _spinner.start() + +def _on_connected(event): + global _spinner + if _spinner: + _spinner.stop() + _spinner = None + d = event.subject or {} + label_connected(f"proxy={d.get('proxy_ip','?')} real={d.get('real_ip','?')} port={d.get('socks5_port','?')}") + +def _on_disconnected(event): + global _spinner + if _spinner: + _spinner.stop() + _spinner = None + d = event.subject or {} + ip = d.get("ip", "?") + label_disconnect(f"real={ip}") + +def _on_error(event): + global _spinner + if _spinner: + _spinner.stop() + _spinner = None + label_error(str(event.subject or "Unknown error")) + +def _on_tor_bootstrapping(event): + global _spinner + _spinner = Spinner("Starting Tor...") + _spinner.start() + +def _on_tor_bootstrap_progressing(event): + pct = int(event.meta.get("progress", 0)) + progress_bar(pct, 100, "Tor bootstrap") + +def _on_tor_bootstrapped(event): + global _spinner + if _spinner: + _spinner.stop() + _spinner = None + print() + label_ok("Tor ready") + +connection_observer.subscribe('connecting', _on_connecting) +connection_observer.subscribe('connected', _on_connected) +connection_observer.subscribe('disconnected', _on_disconnected) +connection_observer.subscribe('error', _on_error) +connection_observer.subscribe('tor_bootstrapping', _on_tor_bootstrapping) +connection_observer.subscribe('tor_bootstrap_progressing', _on_tor_bootstrap_progressing) +connection_observer.subscribe('tor_bootstrapped', _on_tor_bootstrapped) invoice_observer.subscribe('retrieved', lambda event: print(f'\n{pprint.pp(event.subject)}\n')) invoice_observer.subscribe('processing', lambda event: print('A payment has been detected and is being verified...\n')) @@ -34,4 +88,4 @@ invoice_observer.subscribe('settled', lambda event: print('The payment has been profile_observer.subscribe('created', lambda event: pprint.pp((sanitize_profile(event.subject), 'Created'))) profile_observer.subscribe('destroyed', lambda event: pprint.pp((sanitize_profile(event.subject), 'Destroyed'))) profile_observer.subscribe('disabled', lambda event: pprint.pp((sanitize_profile(event.subject), 'Disabled')) if event.meta.get('explicitly') else None) -profile_observer.subscribe('enabled', lambda event: pprint.pp((sanitize_profile(event.subject), 'Enabled'))) +profile_observer.subscribe('enabled', lambda event: pprint.pp((sanitize_profile(event.subject), 'Enabled'))) \ No newline at end of file diff --git a/cli/ui.py b/cli/ui.py new file mode 100644 index 0000000..fe68a7d --- /dev/null +++ b/cli/ui.py @@ -0,0 +1,144 @@ +import sys +import threading +import time + +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" + +RED = "\033[0;31m" +GREEN = "\033[0;32m" +YELLOW = "\033[0;33m" +BLUE = "\033[0;34m" +CYAN = "\033[0;36m" +WHITE = "\033[0;37m" + +BRED = "\033[1;31m" +BGREEN = "\033[1;32m" +BYELLOW = "\033[1;33m" +BBLUE = "\033[1;34m" +BCYAN = "\033[1;36m" +BWHITE = "\033[1;37m" + +def label_ok(msg: str): + print(f" {BGREEN}[ OK ]{RESET} {msg}") + +def label_warn(msg: str): + print(f" {BYELLOW}[ WARN ]{RESET} {msg}") + +def label_error(msg: str): + print(f" {BRED}[ ERROR]{RESET} {msg}") + +def label_info(msg: str): + print(f" {BCYAN}[ INFO ]{RESET} {msg}") + +def label_skip(msg: str): + print(f" {DIM}[ SKIP ]{RESET} {msg}") + +def label_connected(msg: str): + print(f" {BGREEN}[CONNECTED ]{RESET} {msg}") + +def label_disconnect(msg: str): + print(f" {BRED}[DISCONNECTED]{RESET} {msg}") + +def label_step(msg: str, current: int, total: int): + print(f" {BBLUE}[ {current:<2}/{total:<2} ]{RESET} {msg}") + +def print_header(title: str, version: str = ""): + width = 60 + line = "─" * width + ver = f"{DIM}{version}{RESET}" if version else "" + print() + print(f"{BCYAN}{line}{RESET}") + print(f"{BCYAN} {'':<56}{RESET}") + print(f"{BCYAN} {BWHITE}{title} {ver}{'':<40}{BCYAN} {RESET}") + print(f"{BCYAN} {'':<56}{RESET}") + print(f"{BCYAN}{line}{RESET}") + print() + +def print_section(label: str): + print() + print(f"{DIM}{CYAN}┌─{RESET} {BWHITE}{label}{RESET}") + +def print_divider(): + print(f"{DIM}{'─' * 60}{RESET}") + +def progress_bar(current: int, total: int, label: str = "", width: int = 40): + filled = int(current * width / total) + empty = width - filled + bar = "#" * filled + "·" * empty + pct = int(current * 100 / total) + sys.stdout.write( + f"\r {DIM}[{RESET}{BGREEN}{bar}{RESET}{DIM}]{RESET}" + f" {BWHITE}{pct:3d}%{RESET} {DIM}{label}{RESET} " + ) + sys.stdout.flush() + +def progress_simulate(steps: int = 20, label: str = "loading", delay: float = 0.04): + for i in range(1, steps + 1): + progress_bar(i, steps, label) + time.sleep(delay) + print() + +class Spinner: + FRAMES = ("─", "\\", "│", "/") + + def __init__(self, msg: str = "Processing..."): + self.msg = msg + self._stop_event = threading.Event() + self._thread: threading.Thread | None = None + + def start(self): + self._stop_event.clear() + self._thread = threading.Thread(target=self._spin, daemon=True) + self._thread.start() + return self + + def stop(self, result_msg: str = ""): + self._stop_event.set() + if self._thread: + self._thread.join() + sys.stdout.write(f"\r{' ' * 60}\r") + sys.stdout.flush() + if result_msg: + label_ok(result_msg) + + def _spin(self): + i = 0 + while not self._stop_event.is_set(): + frame = self.FRAMES[i % len(self.FRAMES)] + sys.stdout.write( + f"\r {BCYAN}{frame}{RESET} {DIM}{self.msg}{RESET} " + ) + sys.stdout.flush() + i += 1 + time.sleep(0.1) + + def __enter__(self): + self.start() + return self + + def __exit__(self, *_): + self.stop() + + +def wait_with_spinner(msg: str, fn, *args, result_msg: str = "", **kwargs): + + result_box = [None, None] + + def _run(): + try: + result_box[0] = fn(*args, **kwargs) + except Exception as e: + result_box[1] = e + + s = Spinner(msg) + s.start() + t = threading.Thread(target=_run, daemon=True) + t.start() + t.join() + s.stop(result_msg) + + if result_box[1] is not None: + raise result_box[1] + return result_box[0] \ No newline at end of file