diff --git a/INSTALLER/panel.py b/INSTALLER/panel.py new file mode 100755 index 0000000..a3cc7ce --- /dev/null +++ b/INSTALLER/panel.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python3 +""" +OASIS Control Panel — Solar Net Hub +Panel de control compacto estilo Mullvad para gestionar OASIS y ECOIN en Linux. +""" + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GLib, Gdk, GdkPixbuf + +import subprocess +import os +import sys +import threading +import json +import shutil +from pathlib import Path + +# ── Rutas ────────────────────────────────────────────────────────────────── +SCRIPT_DIR = Path(__file__).parent.resolve() +INSTALLER_SH = SCRIPT_DIR / "installer.sh" +OASIS_DIR = Path.home() / "oasis" +ECOIN_DIR = Path.home() / "ecoin" +MODEL_FILE = "oasis-42-1-chat.Q4_K_M.gguf" + +# ── CSS ──────────────────────────────────────────────────────────────────── +CSS = """ +* { + font-family: "Dune Rise", "Cantarell", "Ubuntu", "DejaVu Sans", sans-serif; + color: #E6E6E6; +} + +window { + background-color: #000000; +} + +/* Header */ +box.panel-header { + background-color: #080808; + border-bottom: 1px solid #1C1C1C; +} + +label.panel-title { + font-size: 11pt; + font-weight: bold; + color: #FF4E00; + letter-spacing: 3px; +} + +/* Status card */ +box.status-card { + background-color: #0D0D0D; + border: 1px solid #1C1C1C; + border-radius: 12px; +} + +label.status-main { + font-size: 14pt; + font-weight: bold; +} + +label.state-running { color: #27D980; } +label.state-stopped { color: #FF4E00; } +label.state-unknown { color: #555555; } + +label.status-sub { + font-size: 8pt; + color: #666666; +} + +/* Dots */ +label.dot { font-size: 9pt; } +label.dot-running { color: #27D980; } +label.dot-stopped { color: #FF4E00; } +label.dot-unknown { color: #333333; } + +/* Botones */ +button.btn { + background-color: #000000; + color: #FF4E00; + border: 1px solid #FF4E00; + border-radius: 18px; + padding: 8px 16px; + margin: 5px 6px; + font-size: 8pt; + font-weight: bold; + letter-spacing: 1px; + min-width: 130px; +} + +button.btn:hover { + background-color: #27D980; + color: #000000; + border-color: #27D980; +} + +button.btn:active { + background-color: #1fa865; + color: #000000; + border-color: #1fa865; +} + +button.btn:disabled { + background-color: #080808; + color: #2A2A2A; + border-color: #181818; +} + +button.btn-primary { + background-color: #FF4E00; + color: #000000; + border: 1px solid #FF4E00; + border-radius: 18px; + padding: 8px 16px; + margin: 5px 6px; + font-size: 8pt; + font-weight: bold; + letter-spacing: 1px; + min-width: 130px; +} + +button.btn-primary:hover { + background-color: #27D980; + color: #000000; + border-color: #27D980; +} + +button.btn-primary:disabled { + background-color: #301200; + color: #553322; + border-color: #301200; +} + +/* Notebook / Tabs */ +notebook > header { + background-color: #000000; + border-bottom: 1px solid #1C1C1C; + padding: 0px; +} + +notebook > header tabs { + background-color: #000000; +} + +tab { + background-color: #000000; + color: #444444; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 22px; + font-size: 8pt; + letter-spacing: 1px; + font-weight: bold; +} + +tab:checked { + background-color: #000000; + color: #FF4E00; + border-bottom: 2px solid #FF4E00; +} + +tab:hover { + color: #AAAAAA; +} + +/* Info grid */ +label.info-key { + color: #555555; + font-size: 8pt; + min-width: 90px; +} + +label.info-val { + color: #CCCCCC; + font-size: 8pt; +} + +/* Log area */ +textview.log-view { + background-color: #050505; + color: #27D980; + font-family: "DejaVu Sans Mono", "Monospace", monospace; + font-size: 7.5pt; + padding: 8px; +} + +textview.log-view text { + background-color: #050505; + color: #27D980; +} + +scrolledwindow.log-scroll { + border: 1px solid #1C1C1C; + border-radius: 8px; + margin: 4px 14px 8px 14px; +} + +/* Separadores */ +separator { + background-color: #141414; + min-height: 1px; +} +""" + + +# ── Helpers de estado ────────────────────────────────────────────────────── +def oasis_installed(): + return ( + (OASIS_DIR / "src" / "server" / "node_modules").is_dir() + and (OASIS_DIR / "AI" / MODEL_FILE).is_file() + ) + + +def oasis_version(): + pkg = OASIS_DIR / "src" / "server" / "package.json" + if pkg.is_file(): + try: + return json.loads(pkg.read_text()).get("version", "—") + except Exception: + pass + return "—" + + +def oasis_running(): + try: + r = subprocess.run(["pgrep", "-f", "node.*server.js"], + capture_output=True, timeout=2) + return r.returncode == 0 + except Exception: + return False + + +def ecoin_installed(): + return ( + (ECOIN_DIR / "ecoin" / "ecoin-qt").is_file() + or (ECOIN_DIR / "ecoin" / "src" / "ecoind").is_file() + ) + + +def ecoin_wallet_exists(): + return (Path.home() / ".ecoin" / "wallet.dat").is_file() + + +def node_version(): + try: + r = subprocess.run(["node", "--version"], + capture_output=True, text=True, timeout=3) + return r.stdout.strip() + except Exception: + return "—" + + +# ── Panel principal ──────────────────────────────────────────────────────── +class OasisPanel(Gtk.ApplicationWindow): + + def __init__(self, app): + super().__init__(application=app, title="SOLAR NET HUB") + self.set_default_size(390, 610) + self.set_resizable(False) + self.set_position(Gtk.WindowPosition.CENTER) + + self._build_ui() + # Poll de estado cada 3 s + GLib.timeout_add_seconds(3, self._trigger_poll) + self._trigger_poll() + + # ── Construcción UI ─────────────────────────────────────────────────── + def _build_ui(self): + root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.add(root) + + root.pack_start(self._make_header(), False, False, 0) + + self.notebook = Gtk.Notebook() + self.notebook.set_show_border(False) + root.pack_start(self.notebook, True, True, 0) + + self.notebook.append_page(self._make_oasis_tab(), self._tab_lbl("OASIS")) + self.notebook.append_page(self._make_ecoin_tab(), self._tab_lbl("ECOIN")) + self.notebook.append_page(self._make_sistema_tab(), self._tab_lbl("SISTEMA")) + + def _tab_lbl(self, txt): + return Gtk.Label(label=txt) + + # ── Header ──────────────────────────────────────────────────────────── + def _make_header(self): + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + box.get_style_context().add_class("panel-header") + box.set_margin_top(14) + box.set_margin_bottom(14) + box.set_margin_start(18) + box.set_margin_end(18) + + logo = SCRIPT_DIR / "oasis-logo.png" + if logo.is_file(): + try: + pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(logo), 30, 30, True) + box.pack_start(Gtk.Image.new_from_pixbuf(pb), False, False, 0) + except Exception: + pass + + title = Gtk.Label(label="SOLAR NET HUB") + title.get_style_context().add_class("panel-title") + box.pack_start(title, False, False, 0) + + box.pack_start(Gtk.Box(), True, True, 0) # spacer + + self.header_dot = Gtk.Label(label="●") + self.header_dot.get_style_context().add_class("dot") + self.header_dot.get_style_context().add_class("dot-unknown") + box.pack_end(self.header_dot, False, False, 4) + + return box + + # ── Tab OASIS ───────────────────────────────────────────────────────── + def _make_oasis_tab(self): + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + scroll.add(box) + + # — Status card — + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + card.get_style_context().add_class("status-card") + card.set_margin_top(16) + card.set_margin_bottom(4) + card.set_margin_start(14) + card.set_margin_end(14) + card.set_margin_top(14) + for edge in ("top", "bottom", "start", "end"): + getattr(card, f"set_margin_{edge}")(14) + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row.set_margin_top(14) + row.set_margin_start(16) + row.set_margin_end(16) + + self.oasis_dot = Gtk.Label(label="●") + self.oasis_dot.get_style_context().add_class("dot") + self.oasis_dot.get_style_context().add_class("dot-unknown") + row.pack_start(self.oasis_dot, False, False, 0) + + self.oasis_state_lbl = Gtk.Label(label="Comprobando…") + self.oasis_state_lbl.get_style_context().add_class("status-main") + self.oasis_state_lbl.get_style_context().add_class("state-unknown") + row.pack_start(self.oasis_state_lbl, False, False, 0) + card.pack_start(row, False, False, 0) + + self.oasis_sub_lbl = Gtk.Label(label="") + self.oasis_sub_lbl.get_style_context().add_class("status-sub") + self.oasis_sub_lbl.set_halign(Gtk.Align.START) + self.oasis_sub_lbl.set_margin_start(16) + self.oasis_sub_lbl.set_margin_bottom(14) + card.pack_start(self.oasis_sub_lbl, False, False, 0) + + box.pack_start(card, False, False, 0) + + # — Botones fila 1 — + r1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + r1.set_halign(Gtk.Align.CENTER) + r1.set_margin_top(10) + self.btn_o_start = self._btn("▶ INICIAR", self._on_oasis_start, primary=True) + self.btn_o_stop = self._btn("■ DETENER", self._on_oasis_stop) + r1.pack_start(self.btn_o_start, False, False, 0) + r1.pack_start(self.btn_o_stop, False, False, 0) + box.pack_start(r1, False, False, 0) + + # — Botones fila 2 — + r2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + r2.set_halign(Gtk.Align.CENTER) + r2.set_margin_bottom(6) + self.btn_o_install = self._btn("⬇ INSTALAR", self._on_oasis_install) + self.btn_o_browser = self._btn("◎ ABRIR WEB", self._on_oasis_browser) + r2.pack_start(self.btn_o_install, False, False, 0) + r2.pack_start(self.btn_o_browser, False, False, 0) + box.pack_start(r2, False, False, 0) + + # — Separator + info — + sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + sep.set_margin_top(10) + box.pack_start(sep, False, False, 0) + + grid = Gtk.Grid() + grid.set_column_spacing(14) + grid.set_row_spacing(6) + grid.set_margin_start(18) + grid.set_margin_end(18) + grid.set_margin_top(10) + grid.set_margin_bottom(14) + self.oasis_ver_val = self._info_row(grid, 0, "Versión") + self.oasis_node_val = self._info_row(grid, 1, "Node.js") + self.oasis_dir_val = self._info_row(grid, 2, "Ruta") + box.pack_start(grid, False, False, 0) + + return scroll + + # ── Tab ECOIN ───────────────────────────────────────────────────────── + def _make_ecoin_tab(self): + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + scroll.add(box) + + # — Status card — + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + card.get_style_context().add_class("status-card") + for edge in ("top", "bottom", "start", "end"): + getattr(card, f"set_margin_{edge}")(14) + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row.set_margin_top(14) + row.set_margin_start(16) + row.set_margin_end(16) + + self.ecoin_dot = Gtk.Label(label="●") + self.ecoin_dot.get_style_context().add_class("dot") + self.ecoin_dot.get_style_context().add_class("dot-unknown") + row.pack_start(self.ecoin_dot, False, False, 0) + + self.ecoin_state_lbl = Gtk.Label(label="Comprobando…") + self.ecoin_state_lbl.get_style_context().add_class("status-main") + self.ecoin_state_lbl.get_style_context().add_class("state-unknown") + row.pack_start(self.ecoin_state_lbl, False, False, 0) + card.pack_start(row, False, False, 0) + + self.ecoin_sub_lbl = Gtk.Label(label="") + self.ecoin_sub_lbl.get_style_context().add_class("status-sub") + self.ecoin_sub_lbl.set_halign(Gtk.Align.START) + self.ecoin_sub_lbl.set_margin_start(16) + self.ecoin_sub_lbl.set_margin_bottom(14) + card.pack_start(self.ecoin_sub_lbl, False, False, 0) + + box.pack_start(card, False, False, 0) + + # — Botones fila 1 — + r1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + r1.set_halign(Gtk.Align.CENTER) + r1.set_margin_top(10) + self.btn_e_install = self._btn("⬇ INSTALAR", self._on_ecoin_install, primary=True) + self.btn_e_gui = self._btn("◈ ABRIR GUI", self._on_ecoin_gui) + r1.pack_start(self.btn_e_install, False, False, 0) + r1.pack_start(self.btn_e_gui, False, False, 0) + box.pack_start(r1, False, False, 0) + + # — Botones fila 2 — + r2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + r2.set_halign(Gtk.Align.CENTER) + r2.set_margin_bottom(6) + self.btn_e_wallet = self._btn("✦ CREAR WALLET", self._on_ecoin_wallet) + self.btn_e_connect = self._btn("⟳ CONECTAR", self._on_ecoin_connect) + r2.pack_start(self.btn_e_wallet, False, False, 0) + r2.pack_start(self.btn_e_connect, False, False, 0) + box.pack_start(r2, False, False, 0) + + # — Separator + info — + sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + sep.set_margin_top(10) + box.pack_start(sep, False, False, 0) + + grid = Gtk.Grid() + grid.set_column_spacing(14) + grid.set_row_spacing(6) + grid.set_margin_start(18) + grid.set_margin_end(18) + grid.set_margin_top(10) + grid.set_margin_bottom(14) + self.ecoin_wallet_val = self._info_row(grid, 0, "Wallet") + self.ecoin_qt_val = self._info_row(grid, 1, "ecoin-qt") + self.ecoin_daemon_val = self._info_row(grid, 2, "ecoind") + box.pack_start(grid, False, False, 0) + + return scroll + + # ── Tab SISTEMA ─────────────────────────────────────────────────────── + def _make_sistema_tab(self): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + hdr = Gtk.Label(label="Log de actividad") + hdr.get_style_context().add_class("info-key") + hdr.set_halign(Gtk.Align.START) + hdr.set_margin_start(18) + hdr.set_margin_top(12) + hdr.set_margin_bottom(4) + box.pack_start(hdr, False, False, 0) + + scroll = Gtk.ScrolledWindow() + scroll.get_style_context().add_class("log-scroll") + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scroll.set_vexpand(True) + + self.log_view = Gtk.TextView() + self.log_view.get_style_context().add_class("log-view") + self.log_view.set_editable(False) + self.log_view.set_cursor_visible(False) + self.log_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self.log_buf = self.log_view.get_buffer() + self._end_mark = self.log_buf.create_mark( + "end", self.log_buf.get_end_iter(), False + ) + scroll.add(self.log_view) + box.pack_start(scroll, True, True, 0) + + btn = self._btn("LIMPIAR LOG", self._on_clear_log) + btn.set_margin_start(18) + btn.set_margin_end(18) + btn.set_margin_top(8) + btn.set_margin_bottom(12) + btn.set_halign(Gtk.Align.CENTER) + box.pack_start(btn, False, False, 0) + + return box + + # ── Widget helpers ──────────────────────────────────────────────────── + def _btn(self, label, cb, primary=False): + b = Gtk.Button(label=label) + b.get_style_context().add_class("btn-primary" if primary else "btn") + b.connect("clicked", cb) + return b + + def _info_row(self, grid, row, key): + k = Gtk.Label(label=key) + k.get_style_context().add_class("info-key") + k.set_halign(Gtk.Align.START) + grid.attach(k, 0, row, 1, 1) + + v = Gtk.Label(label="—") + v.get_style_context().add_class("info-val") + v.set_halign(Gtk.Align.START) + grid.attach(v, 1, row, 1, 1) + return v + + # ── Dot / state helpers ─────────────────────────────────────────────── + def _dot(self, lbl, state): + for c in ("dot-running", "dot-stopped", "dot-unknown"): + lbl.get_style_context().remove_class(c) + lbl.get_style_context().add_class(f"dot-{state}") + + def _state(self, lbl, text, state): + for c in ("state-running", "state-stopped", "state-unknown"): + lbl.get_style_context().remove_class(c) + lbl.set_text(text) + lbl.get_style_context().add_class(f"state-{state}") + + # ── Status polling ──────────────────────────────────────────────────── + def _trigger_poll(self): + threading.Thread(target=self._poll_thread, daemon=True).start() + return True + + def _poll_thread(self): + o_inst = oasis_installed() + o_run = oasis_running() + o_ver = oasis_version() + o_node = node_version() + e_inst = ecoin_installed() + e_wall = ecoin_wallet_exists() + e_qt = (ECOIN_DIR / "ecoin" / "ecoin-qt").is_file() + e_dmn = (ECOIN_DIR / "ecoin" / "src" / "ecoind").is_file() + GLib.idle_add(self._apply_status, + o_inst, o_run, o_ver, o_node, + e_inst, e_wall, e_qt, e_dmn) + + def _apply_status(self, o_inst, o_run, o_ver, o_node, + e_inst, e_wall, e_qt, e_dmn): + # — Header dot — + hstate = "running" if o_run else ("stopped" if o_inst else "unknown") + self._dot(self.header_dot, hstate) + + # — OASIS — + self._dot(self.oasis_dot, "running" if o_run else ("stopped" if o_inst else "unknown")) + if o_run: + self._state(self.oasis_state_lbl, "ACTIVO", "running") + self.oasis_sub_lbl.set_text("servidor en puerto 3000") + self.btn_o_start.set_sensitive(False) + self.btn_o_stop.set_sensitive(True) + self.btn_o_browser.set_sensitive(True) + elif o_inst: + self._state(self.oasis_state_lbl, "INSTALADO", "stopped") + self.oasis_sub_lbl.set_text("servidor detenido") + self.btn_o_start.set_sensitive(True) + self.btn_o_stop.set_sensitive(False) + self.btn_o_browser.set_sensitive(False) + else: + self._state(self.oasis_state_lbl, "NO INSTALADO", "unknown") + self.oasis_sub_lbl.set_text("instala OASIS para comenzar") + self.btn_o_start.set_sensitive(False) + self.btn_o_stop.set_sensitive(False) + self.btn_o_browser.set_sensitive(False) + + self.oasis_ver_val.set_text(f"v{o_ver}" if o_ver != "—" else "—") + self.oasis_node_val.set_text(o_node if o_node != "—" else "no instalado") + self.oasis_dir_val.set_text(str(OASIS_DIR) if o_inst else "—") + + # — ECOIN — + self._dot(self.ecoin_dot, "running" if e_qt else ("stopped" if e_inst else "unknown")) + if e_inst: + self._state(self.ecoin_state_lbl, "COMPILADO", "running") + self.ecoin_sub_lbl.set_text("wallet ECOIN disponible") + self.btn_e_gui.set_sensitive(True) + self.btn_e_wallet.set_sensitive(True) + self.btn_e_connect.set_sensitive(True) + else: + self._state(self.ecoin_state_lbl, "NO INSTALADO", "unknown") + self.ecoin_sub_lbl.set_text("instala ECOIN para comenzar") + self.btn_e_gui.set_sensitive(False) + self.btn_e_wallet.set_sensitive(False) + self.btn_e_connect.set_sensitive(False) + + self.ecoin_wallet_val.set_text("Sí" if e_wall else "No") + self.ecoin_qt_val.set_text("Sí" if e_qt else "No") + self.ecoin_daemon_val.set_text("Sí" if e_dmn else "No") + + # ── Acciones ───────────────────────────────────────────────────────── + def _run(self, *args): + """Lanza installer.sh con args, redirige output al log.""" + cmd = [str(INSTALLER_SH)] + list(args) + self._log(f"$ {' '.join(cmd)}") + threading.Thread(target=self._exec_log, args=(cmd,), daemon=True).start() + GLib.idle_add(self.notebook.set_current_page, 2) + + def _exec_log(self, cmd): + try: + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1 + ) + for line in iter(proc.stdout.readline, ""): + GLib.idle_add(self._log, line.rstrip()) + proc.wait() + GLib.idle_add(self._log, f"── proceso terminado (código {proc.returncode}) ──") + except Exception as e: + GLib.idle_add(self._log, f"[Error]: {e}") + GLib.idle_add(self._trigger_poll) + + def _log(self, text): + self.log_buf.insert(self.log_buf.get_end_iter(), text + "\n") + self.log_view.scroll_to_mark(self._end_mark, 0.0, True, 0.0, 1.0) + return False + + # OASIS + def _on_oasis_start(self, _): self._run("--oasis-start") + def _on_oasis_install(self, _): self._run("--oasis-install") + + def _on_oasis_stop(self, _): + self._log("Deteniendo OASIS…") + threading.Thread(target=self._kill_oasis, daemon=True).start() + + def _kill_oasis(self): + try: + subprocess.run(["pkill", "-f", "node.*server.js"], timeout=5) + GLib.idle_add(self._log, "Servidor OASIS detenido.") + except Exception as e: + GLib.idle_add(self._log, f"[Error al detener]: {e}") + GLib.idle_add(self._trigger_poll) + + def _on_oasis_browser(self, _): + subprocess.Popen(["xdg-open", "http://localhost:3000"]) + self._log("Abriendo http://localhost:3000 …") + + # ECOIN + def _on_ecoin_install(self, _): self._run("--ecoin-install") + def _on_ecoin_gui(self, _): self._run("--ecoin-gui") + def _on_ecoin_wallet(self, _): self._run("--wallet-create") + def _on_ecoin_connect(self, _): self._run("--wallet-connect") + + # Sistema + def _on_clear_log(self, _): + self.log_buf.set_text("") + + +# ── Aplicación ───────────────────────────────────────────────────────────── +class OasisApp(Gtk.Application): + def __init__(self): + super().__init__(application_id="net.solarnethub.panel") + + def do_activate(self): + win = OasisPanel(self) + win.show_all() + + +def main(): + # Cargar CSS + provider = Gtk.CssProvider() + provider.load_from_data(CSS.encode("utf-8")) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) + + app = OasisApp() + sys.exit(app.run(sys.argv)) + + +if __name__ == "__main__": + main() diff --git a/start_panel.sh b/start_panel.sh new file mode 100755 index 0000000..7a39f23 --- /dev/null +++ b/start_panel.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# ============================================================= +# SOLAR NET HUB — Panel de control (lanzador) +# Lanza panel.py si python3-gi está disponible; +# si no, lo instala y reintenta. +# ============================================================= +set -euo pipefail + +REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PANEL_PY="$REPO_DIR/INSTALLER/panel.py" +INSTALLER_SH="$REPO_DIR/INSTALLER/installer.sh" + +C_RESET='\033[0m' +C_OK='\033[1;32m' +C_FAIL='\033[1;31m' +C_WARN='\033[1;33m' +C_INFO='\033[1;36m' + +have(){ command -v "$1" >/dev/null 2>&1; } + +# ── Instalar fuente Dune Rise (usuario, sin root) ───────────────────────── +install_font(){ + local src="$REPO_DIR/INSTALLER/Dune_Rise.otf" + local dst="$HOME/.local/share/fonts/Dune_Rise.otf" + if [ -f "$src" ] && [ ! -f "$dst" ]; then + mkdir -p "$HOME/.local/share/fonts" + cp -f "$src" "$dst" + fc-cache -f >/dev/null 2>&1 || true + fi +} + +# ── Verificar / instalar python3-gi ────────────────────────────────────── +check_gi(){ + python3 -c "import gi; gi.require_version('Gtk','3.0'); from gi.repository import Gtk" \ + >/dev/null 2>&1 +} + +install_gi(){ + echo -e "${C_WARN}⚠ python3-gi no encontrado. Instalando…${C_RESET}" + if have apt-get; then sudo apt-get install -y python3-gi python3-gi-cairo gir1.2-gtk-3.0 + elif have pacman; then sudo pacman -Sy --noconfirm python-gobject + elif have dnf; then sudo dnf install -y python3-gobject gtk3 + elif have zypper; then sudo zypper --non-interactive install python3-gobject gtk3 + else + echo -e "${C_FAIL}✘ Gestor de paquetes no soportado.${C_RESET}" + echo " Instala manualmente: python3-gi / python-gobject" + exit 1 + fi +} + +# ── Verificar DISPLAY / Wayland ─────────────────────────────────────────── +check_display(){ + if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then + echo -e "${C_FAIL}✘ No hay sesión gráfica (DISPLAY/WAYLAND_DISPLAY vacíos).${C_RESET}" + echo " Ejecuta este script desde tu escritorio o una terminal gráfica." + exit 1 + fi +} + +# ── Main ───────────────────────────────────────────────────────────────── +echo -e "${C_INFO}== SOLAR NET HUB :: Panel de Control ==${C_RESET}" + +check_display + +install_font 2>/dev/null || true + +if ! check_gi; then + install_gi + if ! check_gi; then + echo -e "${C_FAIL}✘ No se pudo cargar python3-gi tras la instalación.${C_RESET}" + echo " Intenta cerrar sesión y volver a entrar, o instala python3-gi manualmente." + exit 1 + fi +fi + +echo -e "${C_OK}✔ python3-gi listo. Lanzando panel…${C_RESET}" +exec python3 "$PANEL_PY" "$@"