#!/usr/bin/env python3 """ RaveOS Welcome App — First-run onboarding wizard Requires: PyQt6, nmcli (NetworkManager) """ import os import re import sys import subprocess import shutil from pathlib import Path from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QStackedWidget, QLineEdit, QListWidget, QListWidgetItem, QFrame, QGraphicsOpacityEffect, QProgressBar, QSizePolicy, QDialog, QCheckBox, QScrollArea, QFileDialog ) from PyQt6.QtCore import ( Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QPoint, QSize, pyqtProperty, QObject, QProcess ) from PyQt6.QtGui import ( QFont, QFontDatabase, QColor, QPalette, QLinearGradient, QGradient, QPainter, QBrush, QPen, QPixmap, QIcon, QFontMetrics, QPainterPath ) from PyQt6.QtWidgets import QProxyStyle, QStyle from PyQt6.QtSvgWidgets import QSvgWidget # ─── Constants ──────────────────────────────────────────────────────────────── FLAG_FILE = Path.home() / ".config" / "raveos" / ".welcome_done" APP_INSTALLER = "/usr/bin/raveos-app-installer" SVG_LOGO = "/usr/share/raveos-welcome/raveos-welcome.svg" _SCRIPT_DIR = Path(__file__).resolve().parent COLORS = { "bg": "#2B2B2B", "bg2": "#1e1e1e", "surface": "#363636", "surface2": "#404040", "border": "#555555", "accent": "#3d7839", "accent_hover": "#285226", "accent_light": "#5aa856", "text": "#ecf0f1", "text_dim": "#95a5a6", "success": "#3d7839", "warning": "#f39c12", "error": "#e74c3c", } # ─── Translations ───────────────────────────────────────────────────────────── _current_lang = "hu" T = { # Nav "nav_back": {"hu": "← Vissza", "en": "← Back"}, "nav_next": {"hu": "Tovább →", "en": "Next →"}, # Step bar "step_welcome": {"hu": "ÜDVÖZLÉS", "en": "WELCOME"}, "step_network": {"hu": "HÁLÓZAT", "en": "NETWORK"}, "step_optimize": {"hu": "OPTIMALIZÁCIÓ", "en": "OPTIMIZE"}, "step_hyprland": {"hu": "HYPRLAND", "en": "HYPRLAND"}, "step_credits": {"hu": "KÖZREMŰKÖDŐK", "en": "CREDITS"}, "step_finish": {"hu": "BEFEJEZÉS", "en": "FINISH"}, # Welcome page "welcome_tag": {"hu": "LINUX DISTRIBUTION", "en": "LINUX DISTRIBUTION"}, "welcome_about": {"hu": "RaveOS finomhangolás, kulcsra kész állapot!", "en": "RaveOS tuned, ready to go!"}, "btn_support": {"hu": "€ Támogatás", "en": "€ Support"}, "feat_perf": {"hu": "CachyOS kernel\noptimalizált teljesítmény", "en": "CachyOS kernel\noptimized performance"}, "feat_gaming": {"hu": "Gaming-ready\nProton, MangoHud, GameMode", "en": "Gaming-ready\nProton, MangoHud, GameMode"}, "feat_apps": {"hu": "App Installer\negy kattintás, minden szoftver", "en": "App Installer\none click, all software"}, # WiFi page "wifi_title": {"hu": "HÁLÓZAT", "en": "NETWORK"}, "wifi_subtitle": {"hu": "WiFi kapcsolat", "en": "WiFi connection"}, "wifi_scan": {"hu": "Frissítés", "en": "Refresh"}, "wifi_password": {"hu": "Jelszó:", "en": "Password:"}, "wifi_connect": {"hu": "CSATLAKOZÁS", "en": "CONNECT"}, "wifi_scanning": {"hu": "Hálózatok keresése...","en": "Scanning networks..."}, "wifi_none": {"hu": "Nem találtam hálózatot.","en": "No networks found."}, "wifi_found": {"hu": "{n} hálózat található","en": "{n} networks found"}, "wifi_open": {"hu": " [nyílt]", "en": " [open]"}, "wifi_encrypted": {"hu": " [titkosított]", "en": " [encrypted]"}, "wifi_selected": {"hu": "Kiválasztva: {ssid}", "en": "Selected: {ssid}"}, "wifi_connecting": {"hu": "Csatlakozás folyamatban...","en": "Connecting..."}, "wifi_connected": {"hu": "Csatlakozva: {ssid}", "en": "Connected: {ssid}"}, "wifi_error": {"hu": "Hiba: {err}", "en": "Error: {err}"}, "wifi_timeout": {"hu": "Időtúllépés — próbáld újra","en": "Timeout — try again"}, "wifi_unknown_err": {"hu": "Ismeretlen hiba", "en": "Unknown error"}, # Optimize page "opt_title": {"hu": "ALKALMAZÁSOK & OPTIMALIZÁCIÓ", "en": "APPS & OPTIMIZATION"}, "opt_browser": {"hu": "BÖNGÉSZŐ", "en": "BROWSER"}, "opt_brave": {"hu": "Brave Origin", "en": "Brave Origin"}, "opt_brave_desc": {"hu": "Brave Origin — a Brave letisztított, reklám/rewards/kripto-mentes verziója", "en": "Brave Origin — Brave's stripped-down version, without ads/rewards/crypto"}, "opt_firefox": {"hu": "Firefox", "en": "Firefox"}, "opt_firefox_desc": {"hu": "Mozilla Firefox — nyílt forrású, privacy-fókuszú", "en": "Mozilla Firefox — open source, privacy-focused"}, "opt_brave_profile": {"hu": "Brave profil visszaállítása", "en": "Restore Brave profile"}, "opt_brave_profile_desc": {"hu": "Előre konfigurált RaveOS Brave profil másolása (könyvjelzők, beállítások)", "en": "Copy pre-configured RaveOS Brave profile (bookmarks, settings)"}, "opt_gaming": {"hu": "GAMING OPTIMALIZÁCIÓ — GPU: {gpu}", "en": "GAMING OPTIMIZATION — GPU: {gpu}"}, "opt_sysctl": {"hu": "Kernel/sysctl tweaks", "en": "Kernel/sysctl tweaks"}, "opt_sysctl_desc": {"hu": "Kernel paraméterek optimalizálása gaming-hoz:\n" "• vm.swappiness=10 — RAM preferálása swap helyett\n" "• THP=madvise — Transparent Huge Pages, kevesebb micro-stutter\n" "• NMI watchdog kikapcsolva — kevesebb CPU interrupt overhead\n" "• split_lock_mitigate=0 — egyes játékok gyorsabban futnak\n" "• TCP FastOpen — gyorsabb hálózati kapcsolatok online játékhoz\n" "Permanensen mentve: /etc/sysctl.d/99-gaming.conf", "en": "Kernel parameter optimization for gaming:\n" "• vm.swappiness=10 — prefer RAM over swap\n" "• THP=madvise — Transparent Huge Pages, less micro-stutter\n" "• NMI watchdog off — less CPU interrupt overhead\n" "• split_lock_mitigate=0 — some games run faster\n" "• TCP FastOpen — faster network for online gaming\n" "Saved permanently: /etc/sysctl.d/99-gaming.conf"}, "opt_io_sched": {"hu": "I/O Scheduler", "en": "I/O Scheduler"}, "opt_io_sched_desc":{"hu": "Lemez olvasás/írás ütemező optimalizálása:\n" "• SSD → mq-deadline: alacsonyabb latency mint BFQ\n" "• NVMe → none: NVMe saját belső ütemezőt használ, külső felesleges\n" "• HDD → bfq: legjobb forgódobos meghajtóhoz\n" "Permanensen mentve: /etc/udev/rules.d/60-ssd-scheduler.rules", "en": "Disk I/O scheduler optimization:\n" "• SSD → mq-deadline: lower latency than BFQ\n" "• NVMe → none: NVMe uses its own internal scheduler\n" "• HDD → bfq: best for rotational drives\n" "Saved permanently: /etc/udev/rules.d/60-ssd-scheduler.rules"}, "opt_ananicy": {"hu": "ananicy-cpp", "en": "ananicy-cpp"}, "opt_ananicy_desc": {"hu": "Automatikus process prioritás kezelő:\n" "• Játékok indításkor magasabb CPU prioritást kapnak\n" "• Háttérfolyamatok (frissítők, indexelők) alacsonyabb prioritást kapnak\n" "• Eredmény: stabilabb FPS, kevesebb frame drop\n" "Telepíti: ananicy-cpp + cachyos-ananicy-rules, majd engedélyezi", "en": "Automatic process priority handler:\n" "• Games get higher CPU priority on launch\n" "• Background processes (updaters, indexers) get lower priority\n" "• Result: more stable FPS, fewer frame drops\n" "Installs: ananicy-cpp + cachyos-ananicy-rules, then enables"}, "opt_gamemode": {"hu": "GameMode + MangoHud", "en": "GameMode + MangoHud"}, "opt_gamemode_desc":{"hu": "GameMode: automatikus teljesítmény-boost játék közben:\n" "• CPU governor powersave → performance játék indításkor\n" "• Játék bezárásakor visszavált powersave-re\n" "• I/O és process prioritás is optimalizálódik\n\n" "MangoHud: in-game overlay:\n" "• FPS, frametime, GPU/CPU hőmérséklet, VRAM kijelzés\n" "• GOverlay: grafikus felület a MangoHud beállításához\n" "• Steam launch options: MANGOHUD=1 gamemoderun %command%", "en": "GameMode: automatic performance boost while gaming:\n" "• CPU governor powersave → performance on game launch\n" "• Reverts to powersave when game closes\n" "• I/O and process priority also optimized\n\n" "MangoHud: in-game overlay:\n" "• FPS, frametime, GPU/CPU temp, VRAM display\n" "• GOverlay: GUI to configure MangoHud\n" "• Steam launch options: MANGOHUD=1 gamemoderun %command%"}, "opt_gpu_profile": {"hu": "GPU Power Profile — 3D Full Screen", "en": "GPU Power Profile — 3D Full Screen"}, "opt_gpu_profile_desc": {"hu": "AMDGPU teljesítmény profil átváltása 3D_FULL_SCREEN-re:\n" "• Agresszívebb GPU órajel játék közben\n" "• Alapértelmezett profil visszafogottabb teljesítményre törekszik\n" "• Eredmény: magasabb és stabilabb FPS\n" "Permanensen mentve: /etc/tmpfiles.d/gpu-power-profile.conf", "en": "AMDGPU power profile switch to 3D_FULL_SCREEN:\n" "• More aggressive GPU clocks while gaming\n" "• Default profile is more conservative\n" "• Result: higher and more stable FPS\n" "Saved permanently: /etc/tmpfiles.d/gpu-power-profile.conf"}, "opt_amd_overdrive": {"hu": "AMD Overdrive (ppfeaturemask)", "en": "AMD Overdrive (ppfeaturemask)"}, "opt_amd_overdrive_desc": {"hu": "AMD GPU Overdrive funkciók feloldása:\n" "• Lehetővé teszi a manuális órajel és feszültség állítást (LACT-ban)\n" "• Alapértelmezetten az AMD zárolja ezeket a funkciókat\n" "• Beállítás: /etc/modprobe.d/99-amdgpu-overdrive.conf\n" "FIGYELEM: initramfs újraépítést igényel, újraindítás szükséges utána!", "en": "Unlock AMD GPU Overdrive features:\n" "• Enables manual clock and voltage tuning (in LACT)\n" "• AMD locks these features by default\n" "• Config: /etc/modprobe.d/99-amdgpu-overdrive.conf\n" "WARNING: requires initramfs rebuild, reboot needed after!"}, "opt_amd_powercap": {"hu": "AMD GPU Power Cap — Maximum", "en": "AMD GPU Power Cap — Maximum"}, "opt_amd_powercap_desc": {"hu": "AMD GPU teljesítmény korlát feloldása maximumra:\n" "• A GPU alapból korlátozott TDP-re van beállítva\n" "• Ez beállítja a power1_cap értékét a maximálisan engedélyezett értékre\n" "• Eredmény: GPU teljes teljesítményen futhat, magasabb FPS csúcsok\n" "• Permanensen mentve: /etc/tmpfiles.d/amd-powercap.conf", "en": "Unlock AMD GPU power cap to maximum:\n" "• GPU is limited to restricted TDP by default\n" "• Sets power1_cap to the maximum allowed value\n" "• Result: GPU can run at full power, higher FPS peaks\n" "• Saved permanently: /etc/tmpfiles.d/amd-powercap.conf"}, "opt_nvidia_perf": {"hu": "Nvidia Maximum Performance", "en": "Nvidia Maximum Performance"}, "opt_nvidia_perf_desc": {"hu": "Nvidia GPU teljesítmény maximalizálása:\n" "• nvidia-smi persistence mode bekapcsolva (gyorsabb driver válasz)\n" "• Auto-boost letiltva régi kártyákon (stabilabb órajel)\n" "• GPU firmware offload letiltva (NVreg_EnableGpuFirmware=0)\n" "Permanensen mentve: /etc/modprobe.d/99-nvidia-gaming.conf\n" "FIGYELEM: initramfs újraépítést igényel, újraindítás szükséges utána!", "en": "Maximize Nvidia GPU performance:\n" "• nvidia-smi persistence mode on (faster driver response)\n" "• Auto-boost disabled on older cards (more stable clocks)\n" "• GPU firmware offload disabled (NVreg_EnableGpuFirmware=0)\n" "Saved permanently: /etc/modprobe.d/99-nvidia-gaming.conf\n" "WARNING: requires initramfs rebuild, reboot needed after!"}, "opt_apply": {"hu": "TELEPÍTÉS / ALKALMAZÁS", "en": "INSTALL / APPLY"}, "opt_apply_empty": {"hu": "Nincs kijelölve semmi.", "en": "Nothing selected."}, "opt_apply_missing":{"hu": "Hiányzó script: ", "en": "Missing script: "}, "opt_apply_ok": {"hu": "Sikeresen alkalmazva!", "en": "Successfully applied!"}, "opt_apply_err": {"hu": "Hiba (kód: {code}) — napló: {log}", "en": "Error (code: {code}) — log: {log}"}, # Hyprland page "hypr_title": {"hu": "HYPRLAND — KEYBINDS", "en": "HYPRLAND — KEYBINDS"}, "hypr_source": {"hu": "Forrás: ", "en": "Source: "}, "hypr_none": {"hu": "Nem található Hyprland konfiguráció.", "en": "No Hyprland configuration found."}, # Hyprland descriptions "hypr_spotlight": {"hu": "Spotlight (DMS) megnyitása", "en": "Open Spotlight (DMS)"}, "hypr_terminal": {"hu": "Terminál megnyitása (Kitty)", "en": "Open terminal (Kitty)"}, "hypr_filemanager": {"hu": "Fájlkezelő megnyitása (Thunar)", "en": "Open file manager (Thunar)"}, "hypr_launcher": {"hu": "App launcher megnyitása", "en": "Open app launcher"}, "hypr_logout": {"hu": "Kijelentkezés / leállítás", "en": "Logout / shutdown"}, "hypr_exit": {"hu": "Hyprland kilépés", "en": "Exit Hyprland"}, "hypr_volume": {"hu": "Hangerő állítás", "en": "Adjust volume"}, "hypr_mute": {"hu": "Némítás ki/be", "en": "Toggle mute"}, "hypr_brightness": {"hu": "Fényerő állítás", "en": "Adjust brightness"}, "hypr_next_track": {"hu": "Következő szám", "en": "Next track"}, "hypr_prev_track": {"hu": "Előző szám", "en": "Previous track"}, "hypr_play_pause": {"hu": "Lejátszás / szünet", "en": "Play / pause"}, "hypr_clipboard": {"hu": "Vágólap megjelenítése", "en": "Show clipboard"}, "hypr_screenshot": {"hu": "Képernyőkép készítése", "en": "Take screenshot"}, "hypr_lock": {"hu": "Képernyőzár", "en": "Lock screen"}, "hypr_kill": {"hu": "Aktív ablak bezárása", "en": "Close active window"}, "hypr_float": {"hu": "Lebegő mód ki/be", "en": "Toggle floating mode"}, "hypr_pseudo": {"hu": "Pseudo-tiling ki/be", "en": "Toggle pseudo-tiling"}, "hypr_fullscreen": {"hu": "Teljes képernyős mód", "en": "Fullscreen mode"}, "hypr_exit2": {"hu": "Kilépés", "en": "Exit"}, "hypr_scratchpad": {"hu": "Scratchpad megjelenítése", "en": "Toggle scratchpad"}, "hypr_focus_l": {"hu": "Fókusz mozgatása balra", "en": "Focus left"}, "hypr_focus_r": {"hu": "Fókusz mozgatása jobbra", "en": "Focus right"}, "hypr_focus_u": {"hu": "Fókusz mozgatása fel", "en": "Focus up"}, "hypr_focus_d": {"hu": "Fókusz mozgatása le", "en": "Focus down"}, "hypr_ws_goto": {"hu": "Váltás {p}. munkaterületre", "en": "Switch to workspace {p}"}, "hypr_ws_move": {"hu": "Ablak áthelyezése {p}. munkaterületre", "en": "Move window to workspace {p}"}, "hypr_ws_generic": {"hu": "Munkaterület: {p}", "en": "Workspace: {p}"}, "hypr_ws_scratch": {"hu": "Ablak → scratchpad", "en": "Window → scratchpad"}, "hypr_ws_scratch_t":{"hu": "Scratchpad ki/be", "en": "Toggle scratchpad"}, "hypr_ws_next": {"hu": "Következő munkaterület", "en": "Next workspace"}, "hypr_ws_prev": {"hu": "Előző munkaterület", "en": "Previous workspace"}, "hypr_ws_goto_n": {"hu": "Váltás {n}. munkaterületre", "en": "Switch to workspace {n}"}, "hypr_ws_move_n": {"hu": "Ablak áthelyezése {n}. munkaterületre", "en": "Move window to workspace {n}"}, "hypr_layout": {"hu": "Layout: {p}", "en": "Layout: {p}"}, "hypr_run": {"hu": "Futtatás: {prog}", "en": "Run: {prog}"}, "hypr_run_generic": {"hu": "Program futtatása", "en": "Run program"}, # Credits page "credits_title": {"hu": "KÖZREMŰKÖDŐK", "en": "CONTRIBUTORS"}, "credits_rp1_name": {"hu": "RavePriest1", "en": "RavePriest1"}, "credits_rp1_role": {"hu": "Névadó, hivatalos naplopó", "en": "Namesake, official slacker"}, "credits_rp1_desc": {"hu": "Nem tudja mit akar — magyar streamer.", "en": "Doesn't know what he wants — Hungarian streamer."}, "credits_alexc_name": {"hu": "AlexC", "en": "AlexC"}, "credits_alexc_role": {"hu": "Arch alapkő", "en": "Arch cornerstone"}, "credits_alexc_desc": {"hu": "Hallóóó mester.", "en": "Hellooo master."}, "credits_nippy_name": {"hu": "Nippy", "en": "Nippy"}, "credits_nippy_role": {"hu": "Fejlesztő", "en": "Developer"}, "credits_nippy_desc": {"hu": "Agyérgörcsöt kap PR1-től, mert nem tudja mit akar.", "en": "Gets an aneurysm from PR1 because he doesn't know what he wants."}, "credits_gabesz_name": {"hu": "GabeszM", "en": "GabeszM"}, "credits_gabesz_role": {"hu": "Fejlesztő", "en": "Developer"}, "credits_gabesz_desc": {"hu": "Weboldal, GNOME kiegészítő és alt-tab dizájn — mert valakinek kellett.", "en": "Website, GNOME extension and alt-tab design — because someone had to."}, "credits_stefi_name": {"hu": "Stefi", "en": "Stefi"}, "credits_stefi_role": {"hu": "Design", "en": "Design"}, "credits_stefi_desc": {"hu": "5000 lesz!", "en": "That'll be 5000!"}, # Finish page "finish_ok": {"hu": "OK", "en": "OK"}, "finish_title": {"hu": "KÉSZ!", "en": "DONE!"}, "finish_sub": {"hu": "A RaveOS be van állítva.", "en": "RaveOS is set up."}, "finish_next_step": {"hu": "KÖVETKEZŐ LÉPÉS", "en": "NEXT STEP"}, "finish_inst_desc": {"hu": "Az App Installer segítségével telepíthetsz minden fontos " "szoftvert egy helyen — böngészőtől kezdve a fejlesztőeszközökig.", "en": "With the App Installer you can install all important " "software in one place — from browsers to developer tools."}, "finish_install": {"hu": "APP INSTALLER INDÍTÁSA", "en": "LAUNCH APP INSTALLER"}, "finish_close": {"hu": "Bezárás", "en": "Close"}, "finish_install_missing": {"hu": "Nem található az App Installer.", "en": "App Installer not found."}, "finish_install_err": {"hu": "Nem sikerült elindítani. Napló: {log}", "en": "Failed to launch. Log: {log}"}, # Tooltip keys (for optimize page descriptions that use _ACTION_DESCS) "action_killactive": {"hu": "Aktív ablak bezárása", "en": "Close active window"}, "action_togglefloat": {"hu": "Lebegő mód ki/be", "en": "Toggle floating mode"}, "action_pseudo": {"hu": "Pseudo-tiling ki/be", "en": "Toggle pseudo-tiling"}, "action_fullscreen": {"hu": "Teljes képernyős mód", "en": "Fullscreen mode"}, "action_exit": {"hu": "Kilépés", "en": "Exit"}, "action_scratchpad": {"hu": "Scratchpad megjelenítése", "en": "Toggle scratchpad"}, "action_focus_l": {"hu": "Fókusz mozgatása balra", "en": "Focus left"}, "action_focus_r": {"hu": "Fókusz mozgatása jobbra", "en": "Focus right"}, "action_focus_u": {"hu": "Fókusz mozgatása fel", "en": "Focus up"}, "action_focus_d": {"hu": "Fókusz mozgatása le", "en": "Focus down"}, } def _t(key, **kwargs): """Get translation for key, with optional format kwargs.""" lang = _current_lang text = T.get(key, {}).get(lang, T.get(key, {}).get("hu", key)) if kwargs: try: return text.format(**kwargs) except (KeyError, IndexError): return text return text STYLESHEET = f""" QMainWindow, QWidget#central {{ background: {COLORS['bg']}; }} QWidget {{ color: {COLORS['text']}; font-family: 'Ubuntu', sans-serif; }} QLabel#title {{ font-size: 38px; font-weight: 700; color: {COLORS['text']}; }} QLabel#subtitle {{ font-size: 14px; color: {COLORS['text_dim']}; letter-spacing: 1px; }} QLabel#section_title {{ font-size: 12px; font-weight: 600; letter-spacing: 2px; color: {COLORS['accent_light']}; text-transform: uppercase; }} QLabel#body {{ font-size: 14px; color: {COLORS['text_dim']}; line-height: 1.6; }} QPushButton#primary {{ background: {COLORS['accent']}; color: white; border: none; border-radius: 6px; padding: 12px 32px; font-size: 13px; font-weight: 600; letter-spacing: 1px; min-width: 140px; }} QPushButton#primary:hover {{ background: {COLORS['accent_hover']}; }} QPushButton#primary:pressed {{ background: {COLORS['accent_hover']}; padding: 13px 31px 11px 33px; }} QPushButton#primary:disabled {{ background: {COLORS['surface2']}; color: {COLORS['text_dim']}; }} QPushButton#secondary {{ background: transparent; color: {COLORS['text_dim']}; border: 1px solid {COLORS['border']}; border-radius: 6px; padding: 12px 32px; font-size: 13px; font-weight: 500; min-width: 100px; }} QPushButton#secondary:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }} QPushButton#icon_btn {{ background: {COLORS['surface']}; border: 1px solid {COLORS['border']}; border-radius: 6px; padding: 8px 14px; font-size: 13px; color: {COLORS['text_dim']}; }} QPushButton#icon_btn:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; background: {COLORS['surface2']}; }} QListWidget {{ background: {COLORS['surface']}; border: 1px solid {COLORS['border']}; border-radius: 8px; padding: 4px; outline: none; }} QListWidget::item {{ padding: 10px 14px; border-radius: 4px; font-size: 14px; color: {COLORS['text']}; border: none; }} QListWidget::item:hover {{ background: {COLORS['surface2']}; }} QListWidget::item:selected {{ background: rgba(61, 120, 57, 0.20); color: {COLORS['accent_light']}; border: 1px solid rgba(61, 120, 57, 0.40); }} QLineEdit {{ background: {COLORS['surface']}; border: 1px solid {COLORS['border']}; border-radius: 6px; padding: 10px 14px; font-size: 14px; color: {COLORS['text']}; }} QLineEdit:focus {{ border-color: {COLORS['accent']}; }} QLineEdit::placeholder {{ color: {COLORS['text_dim']}; }} QProgressBar {{ background: {COLORS['surface']}; border: none; border-radius: 2px; height: 4px; text-align: center; font-size: 0px; }} QProgressBar::chunk {{ background: {COLORS['accent']}; border-radius: 2px; }} QFrame#separator {{ background: {COLORS['accent']}; max-height: 1px; min-height: 1px; opacity: 0.4; }} QFrame#card {{ background: {COLORS['surface']}; border: 1px solid {COLORS['border']}; border-radius: 8px; }} QToolTip {{ background: {COLORS['bg2']}; color: {COLORS['text']}; border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 6px 10px; font-size: 12px; }} """ # ─── WiFi Worker ─────────────────────────────────────────────────────────────── class WiFiScanner(QThread): result = pyqtSignal(list) error = pyqtSignal(str) def run(self): try: out = subprocess.check_output( ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY", "dev", "wifi", "list", "--rescan", "yes"], stderr=subprocess.DEVNULL, timeout=10 ).decode().strip() networks = [] seen = set() for line in out.splitlines(): parts = line.split(":") if len(parts) >= 2: ssid = parts[0].strip() if not ssid or ssid in seen: continue seen.add(ssid) try: signal = int(parts[1]) except ValueError: signal = 0 security = parts[2].strip() if len(parts) > 2 else "" networks.append({"ssid": ssid, "signal": signal, "security": security}) networks.sort(key=lambda x: x["signal"], reverse=True) self.result.emit(networks) except Exception as e: self.error.emit(str(e)) class WiFiConnector(QThread): success = pyqtSignal() failure = pyqtSignal(str) def __init__(self, ssid, password=""): super().__init__() self.ssid = ssid self.password = password def run(self): try: if self.password: cmd = ["nmcli", "dev", "wifi", "connect", self.ssid, "password", self.password] else: cmd = ["nmcli", "dev", "wifi", "connect", self.ssid] result = subprocess.run(cmd, capture_output=True, text=True, timeout=20) if result.returncode == 0: self.success.emit() else: self.failure.emit(result.stderr.strip() or _t("wifi_unknown_err")) except subprocess.TimeoutExpired: self.failure.emit(_t("wifi_timeout")) except Exception as e: self.failure.emit(str(e)) # ─── Logo Widget ─────────────────────────────────────────────────────────────── def _find_svg() -> str: if Path(SVG_LOGO).exists(): return SVG_LOGO candidate = _SCRIPT_DIR / "raveos-welcome.svg" if candidate.exists(): return str(candidate) return "" class RaveLogo(QWidget): """RaveOS SVG logo, nagy meretben.""" SIZE = 180 def __init__(self, parent=None): super().__init__(parent) self.setFixedSize(self.SIZE, self.SIZE) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) svg_path = _find_svg() if svg_path: self._svg = QSvgWidget(svg_path, self) self._svg.setFixedSize(self.SIZE, self.SIZE) layout.addWidget(self._svg) else: fallback = QLabel("RAVE OS", self) fallback.setAlignment(Qt.AlignmentFlag.AlignCenter) fallback.setStyleSheet( f"font-size: 32px; font-weight: 700; color: {COLORS['text']};" f"font-family: Ubuntu;" ) layout.addWidget(fallback) # ─── WiFi page visibility check ─────────────────────────────────────────────── def _should_show_wifi() -> bool: try: out = subprocess.check_output( ["nmcli", "-t", "-f", "TYPE,STATE", "dev"], stderr=subprocess.DEVNULL, timeout=1 ).decode() except Exception: return False has_wifi = False for line in out.splitlines(): parts = line.split(":") if len(parts) < 2: continue dev_type, state = parts[0], parts[1] if dev_type == "wifi": has_wifi = True if dev_type == "ethernet" and state == "connected": return False # aktív vezetékes kapcsolat van, wifi oldal nem kell return has_wifi # ─── Signal bar helper ───────────────────────────────────────────────────────── def signal_icon(signal: int) -> str: if signal >= 80: return "▂▄▆█" if signal >= 60: return "▂▄▆·" if signal >= 40: return "▂▄··" return "▂···" def security_tag(sec: str) -> str: if not sec or sec == "--": return _t("wifi_open") return _t("wifi_encrypted") # ─── Page 1: Welcome ─────────────────────────────────────────────────────────── class WelcomePage(QWidget): def __init__(self): super().__init__() self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 40, 60, 40) lay.setSpacing(0) # Logo logo = RaveLogo() lay.addWidget(logo, alignment=Qt.AlignmentFlag.AlignHCenter) lay.addSpacing(10) title_lbl = QLabel() title_lbl.setTextFormat(Qt.TextFormat.RichText) title_lbl.setText( f'RAVE' f'OS' ) title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(title_lbl) lay.addSpacing(2) self.tag = QLabel(_t("welcome_tag")) self.tag.setObjectName("subtitle") self.tag.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.tag) sep = QFrame() sep.setObjectName("separator") sep.setFixedHeight(1) lay.addSpacing(28) lay.addWidget(sep) lay.addSpacing(24) # About card card = QFrame() card.setObjectName("card") card_lay = QVBoxLayout(card) card_lay.setContentsMargins(28, 20, 28, 20) card_lay.setSpacing(14) self.about = QLabel(_t("welcome_about")) self.about.setObjectName("body") self.about.setAlignment(Qt.AlignmentFlag.AlignCenter) self.about.setWordWrap(True) card_lay.addWidget(self.about) sep2 = QFrame() sep2.setObjectName("separator") sep2.setFixedHeight(1) card_lay.addWidget(sep2) features_lay = QHBoxLayout() features_lay.setSpacing(24) self._feature_icons = [] self._feature_labels = [] for icon, key in [ ("⚡︎", "feat_perf"), ("🎮︎", "feat_gaming"), ("🛠︎", "feat_apps"), ]: col = QVBoxLayout() col.setSpacing(4) ico = QLabel(icon) ico.setAlignment(Qt.AlignmentFlag.AlignCenter) ico.setStyleSheet(f"font-size: 22px; color: {COLORS['text']};") lbl = QLabel(_t(key)) lbl.setObjectName("body") lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lbl.setStyleSheet(f"font-size: 11px; color: {COLORS['text_dim']};") col.addWidget(ico) col.addWidget(lbl) features_lay.addLayout(col) self._feature_icons.append(icon) self._feature_labels.append((key, lbl)) card_lay.addLayout(features_lay) lay.addWidget(card) lay.addSpacing(18) # Community links links_row1 = QHBoxLayout() links_row1.setSpacing(8) links_row2 = QHBoxLayout() links_row2.setSpacing(8) all_links = [ ("support", "https://ko-fi.com/ravepriest1/tiers"), ("RP Forgejo", "https://git.rp1.hu/explore/repos"), ("RP YouTube", "https://www.youtube.com/@RPslair"), ("RP Twitch", "https://www.twitch.tv/ravepriest1"), ("RP Kick", "https://kick.com/rpslair"), ("RP Discord", "https://discord.gg/gSdVMXRFQc"), ] self._link_btns = [] self._support_btn = None for i, (label, url) in enumerate(all_links): if label == "support": btn = QPushButton(_t("btn_support")) self._support_btn = btn else: btn = QPushButton(label) btn.setObjectName("icon_btn") btn.clicked.connect(lambda _, u=url: self._open_url(u)) if label == "support": btn.setStyleSheet( f"QPushButton {{ background: {COLORS['surface']}; border: 1px solid {COLORS['accent']};" f" border-radius: 6px; padding: 8px 14px; font-size: 13px;" f" color: #ffffff; font-weight: 700; }}" f"QPushButton:hover {{ border-color: {COLORS['accent_light']};" f" color: {COLORS['accent_light']}; background: {COLORS['surface2']}; }}" ) if i < 3: links_row1.addWidget(btn) else: links_row2.addWidget(btn) self._link_btns.append((label, btn)) lay.addLayout(links_row1) lay.addSpacing(6) lay.addLayout(links_row2) lay.addStretch() def retranslate(self): self.tag.setText(_t("welcome_tag")) self.about.setText(_t("welcome_about")) for key, lbl in self._feature_labels: lbl.setText(_t(key)) if self._support_btn: self._support_btn.setText(_t("btn_support")) def _open_url(self, url): subprocess.Popen(["xdg-open", url]) # ─── Page 2: WiFi ────────────────────────────────────────────────────────────── class WiFiPage(QWidget): def __init__(self): super().__init__() self._networks = [] self._selected_ssid = None self._scanner = None self._connector = None self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 40, 60, 40) lay.setSpacing(16) self.title = QLabel(_t("wifi_title")) self.title.setObjectName("section_title") lay.addWidget(self.title) h = QHBoxLayout() h.setSpacing(10) self.net_title = QLabel(_t("wifi_subtitle")) self.net_title.setObjectName("subtitle") h.addWidget(self.net_title) h.addStretch() self.scan_btn = QPushButton(_t("wifi_scan")) self.scan_btn.setObjectName("icon_btn") self.scan_btn.clicked.connect(self.scan) h.addWidget(self.scan_btn) lay.addLayout(h) self.progress = QProgressBar() self.progress.setRange(0, 0) self.progress.setFixedHeight(4) self.progress.setVisible(False) lay.addWidget(self.progress) self.list = QListWidget() self.list.setMinimumHeight(180) self.list.itemClicked.connect(self._on_select) lay.addWidget(self.list) # Password field self.pw_frame = QFrame() pw_lay = QVBoxLayout(self.pw_frame) pw_lay.setContentsMargins(0, 0, 0, 0) pw_lay.setSpacing(6) self.pw_label = QLabel(_t("wifi_password")) self.pw_label.setObjectName("body") pw_lay.addWidget(self.pw_label) self.pw_input = QLineEdit() self.pw_input.setPlaceholderText(_t("wifi_password")) self.pw_input.setEchoMode(QLineEdit.EchoMode.Password) pw_lay.addWidget(self.pw_input) self.pw_frame.setVisible(False) lay.addWidget(self.pw_frame) # Status self.status_label = QLabel("") self.status_label.setObjectName("body") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.status_label) # Connect button self.connect_btn = QPushButton(_t("wifi_connect")) self.connect_btn.setObjectName("primary") self.connect_btn.setEnabled(False) self.connect_btn.clicked.connect(self._connect) lay.addWidget(self.connect_btn, alignment=Qt.AlignmentFlag.AlignRight) lay.addStretch() def retranslate(self): self.title.setText(_t("wifi_title")) self.net_title.setText(_t("wifi_subtitle")) self.scan_btn.setText(_t("wifi_scan")) self.connect_btn.setText(_t("wifi_connect")) if self._selected_ssid: self.status_label.setText(_t("wifi_selected", ssid=self._selected_ssid)) def showEvent(self, event): super().showEvent(event) if not self._networks: self.scan() def scan(self): self.scan_btn.setEnabled(False) self.list.clear() self.status_label.setText(_t("wifi_scanning")) self.progress.setVisible(True) self._scanner = WiFiScanner() self._scanner.result.connect(self._on_scan_result) self._scanner.error.connect(self._on_scan_error) self._scanner.start() def _on_scan_result(self, networks): self.progress.setVisible(False) self.scan_btn.setEnabled(True) self._networks = networks self.list.clear() if not networks: self.status_label.setText(_t("wifi_none")) return self.status_label.setText(_t("wifi_found", n=len(networks))) for n in networks: text = f"{signal_icon(n['signal'])} {n['ssid']}{security_tag(n['security'])}" item = QListWidgetItem(text) item.setData(Qt.ItemDataRole.UserRole, n) self.list.addItem(item) def _on_scan_error(self, err): self.progress.setVisible(False) self.scan_btn.setEnabled(True) self.status_label.setStyleSheet(f"color: {COLORS['error']};") self.status_label.setText(_t("wifi_error", err=err)) def _on_select(self, item): n = item.data(Qt.ItemDataRole.UserRole) self._selected_ssid = n["ssid"] needs_pw = bool(n["security"] and n["security"] != "--") self.pw_frame.setVisible(needs_pw) if needs_pw: self.pw_label.setText(f"{_t('wifi_password')} ({n['ssid']}):") self.connect_btn.setEnabled(True) self.status_label.setStyleSheet("") self.status_label.setText(_t("wifi_selected", ssid=n['ssid'])) def _connect(self): if not self._selected_ssid: return pw = self.pw_input.text() if self.pw_frame.isVisible() else "" self.connect_btn.setEnabled(False) self.progress.setVisible(True) self.status_label.setStyleSheet(f"color: {COLORS['text_dim']};") self.status_label.setText(_t("wifi_connecting")) self._connector = WiFiConnector(self._selected_ssid, pw) self._connector.success.connect(self._on_connected) self._connector.failure.connect(self._on_connect_fail) self._connector.start() def _on_connected(self): self.progress.setVisible(False) self.status_label.setStyleSheet(f"color: {COLORS['success']};") self.status_label.setText(_t("wifi_connected", ssid=self._selected_ssid)) self.connect_btn.setEnabled(True) def _on_connect_fail(self, err): self.progress.setVisible(False) self.status_label.setStyleSheet(f"color: {COLORS['error']};") self.status_label.setText(_t("wifi_error", err=err)) self.connect_btn.setEnabled(True) # ─── Hyprland Detection ─────────────────────────────────────────────────────── HYPR_CONFIG = Path.home() / ".config" / "hypr" / "hyprland.conf" _HYPR_FALLBACK = Path("/usr/share/raveos/hyprland-theme/theme-data/hypr/hyprland.conf") # Hyprland 0.55+ Lua config mod (RaveOS ezt hasznalja alapertelmezetten, # nincs is hagyomanyos hyprland.conf a rendszeren) HYPR_KEYBINDS_LUA = Path.home() / ".config" / "hypr" / "config" / "keybinds.lua" _HYPR_KEYBINDS_LUA_FALLBACK = Path( "/usr/share/raveos/hyprland-theme/theme-data/hypr/config/keybinds.lua" ) def _detect_hyprland() -> bool: if os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == "hyprland": return True if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"): return True return False def _fmt_key(raw: str) -> str: mapping = { "super": "Win", "ctrl": "Ctrl", "alt": "Alt", "shift": "Shift", "return": "Enter", "space": "Space", "tab": "Tab", "left": "←", "right": "→", "up": "↑", "down": "↓", "mouse_down": "Görgő le", "mouse_up": "Görgő fel", "mouse:272": "Bal egér", "mouse:273": "Jobb egér", "super_l": "Win", } parts = [p.strip() for p in raw.replace("+", " ").split() if p.strip()] result = [] for p in parts: result.append(mapping.get(p.lower(), p.upper())) return " + ".join(result) _EXEC_SNIPPETS = { "dms ipc call spotlight toggle": "Spotlight (DMS) megnyitása", "dms ipc call clipboard toggle": "Vágólap megjelenítése", "dms ipc call processlist toggle": "Folyamatlista megjelenítése", "dms ipc call notifications toggle": "Értesítések megjelenítése", "dms ipc call settings toggle": "Beállítások megnyitása", "dms ipc call dankdash wallpaper": "Háttérkép váltó megnyitása", "hyprshell socat": "Ablakváltó (Alt+Tab) megnyitása", "reboot": "Újraindítás", "poweroff": "Leállítás", "freetube": "FreeTube megnyitása", "discord": "Discord megnyitása", "gedit": "Szövegszerkesztő megnyitása", "pavucontrol": "Hangkezelő megnyitása", "kitty": "Terminál megnyitása (Kitty)", "thunar": "Fájlkezelő megnyitása (Thunar)", "hyperlauncher": "App launcher megnyitása", "hyprlauncher": "App launcher megnyitása", "hyprshutdown": "Kijelentkezés / leállítás", "hyprctl dispatch exit": "Hyprland kilépés", "wpctl set-volume": "Hangerő állítás", "wpctl set-mute": "Némítás ki/be", "brightnessctl": "Fényerő állítás", "playerctl next": "Következő szám", "playerctl previous": "Előző szám", "playerctl play-pause": "Lejátszás / szünet", "rofi": "App launcher (Rofi)", "wofi": "App launcher (Wofi)", "wl-paste": "Vágólap kezelő", "cliphist": "Vágólap megjelenítése", "grim": "Képernyőkép készítése", "grimblast": "Képernyőkép készítése", "hyprlock": "Képernyőzár", "swaylock": "Képernyőzár", } _ACTION_DESCS = { "killactive": "Aktív ablak bezárása", "togglefloating": "Lebegő mód ki/be", "pseudo": "Pseudo-tiling ki/be", "fullscreen": "Teljes képernyős mód", "exit": "Kilépés", "movefocus": { "l": "Fókusz mozgatása balra", "r": "Fókusz mozgatása jobbra", "u": "Fókusz mozgatása fel", "d": "Fókusz mozgatása le", }, "togglespecialworkspace": "Scratchpad megjelenítése", "workspace": lambda p: ( f"Váltás {p}. munkaterületre" if p.isdigit() else f"Munkaterület: {p}" ), "movetoworkspace": lambda p: ( f"Ablak áthelyezése {p}. munkaterületre" if p.isdigit() else f"Ablak áthelyezése: {p}" ), "layoutmsg": lambda p: f"Layout: {p}", } def _exec_desc(cmd: str) -> str: cmd_lower = cmd.lower() for snippet, label in _EXEC_SNIPPETS.items(): if snippet.lower() in cmd_lower: return label prog = cmd.strip().split()[0].split("/")[-1] if cmd.strip() else "" return f"Futtatás: {prog}" if prog else "Program futtatása" def _workspace_desc(prefix: str, params: str) -> str: p = params.strip() if p.startswith("special:"): return "Ablak → scratchpad" if prefix == "move" else "Scratchpad ki/be" if p in ("e+1",): return "Következő munkaterület" if p in ("e-1",): return "Előző munkaterület" if p.isdigit(): n = p if p != "0" else "10" return f"Ablak → {n}. munkaterületre" if prefix == "move" else f"Váltás {n}. munkaterületre" return f"Munkaterület: {p}" def _action_desc(action: str, params: str) -> str: if action in ("workspace",): return _workspace_desc("", params) if action in ("movetoworkspace",): return _workspace_desc("move", params) entry = _ACTION_DESCS.get(action) if entry is None: if action == "exec": return _exec_desc(params) return f"{action} {params}".strip() if callable(entry): return entry(params) if isinstance(entry, dict): return entry.get(params.strip(), f"Fókusz mozgatása ({params})") return entry def _parse_hypr_binds(config_path: Path) -> list: if not config_path.exists(): return [] try: text = config_path.read_text(errors="replace") except Exception: return [] vars_ = {} binds = [] for line in text.splitlines(): line = line.strip() if not line or line.startswith("#"): continue if line.startswith("$") and "=" in line: k, _, v = line.partition("=") vars_[k.strip()] = v.split("#")[0].strip() continue if not line.startswith("bind"): continue if line.startswith("bindm"): continue eq = line.find("=") if eq < 0: continue rest = line[eq + 1:].strip() for vn, vv in vars_.items(): rest = rest.replace(vn, vv) parts = [p.strip() for p in rest.split(",")] if len(parts) < 3: continue mods_raw = parts[0] key_raw = parts[1] action = parts[2].lower().strip() if action == "exec" and len(parts) > 3: params = ",".join(parts[3:]).split("#")[0].strip() elif len(parts) > 3: params = parts[3].split("#")[0].strip() else: params = "" if "XF86" in key_raw: continue combo = _fmt_key(f"{mods_raw} {key_raw}" if mods_raw else key_raw) desc = _action_desc(action, params) if combo and desc: binds.append((combo, desc)) return binds # ─── Hyprland Lua config parser (keybinds.lua, hl.bind(...) hivasok) ───────── # Best-effort parser: nem valodi Lua interpreter, csak a RaveOS keybinds.lua # tipikus mintait (hl.bind("KEY", hl.dsp.xxx(...)), local string valtozok, # egyszeru "for i = a, b do ... end" munkaterulet-ciklusok) ismeri fel. def _lua_extract_call(line: str, funcname: str): marker = funcname + "(" idx = line.find(marker) if idx < 0: return None start = idx + len(marker) depth = 1 i = start in_str = False str_char = "" while i < len(line) and depth > 0: c = line[i] if in_str: if c == str_char: in_str = False elif c in "\"'": in_str = True str_char = c elif c in "([{": depth += 1 elif c in ")]}": depth -= 1 i += 1 if depth != 0: return None return line[start:i - 1] def _lua_split_args(s: str) -> list: parts = [] depth = 0 current = [] in_str = False str_char = "" for c in s: if in_str: current.append(c) if c == str_char: in_str = False continue if c in "\"'": in_str = True str_char = c current.append(c) continue if c in "([{": depth += 1 current.append(c) continue if c in ")]}": depth -= 1 current.append(c) continue if c == "," and depth == 0: parts.append("".join(current)) current = [] continue current.append(c) if current: parts.append("".join(current)) return [p.strip() for p in parts] def _lua_resolve_key_expr(expr: str, env: dict): parts = expr.split("..") resolved = [] for part in parts: part = part.strip() if len(part) >= 2 and part[0] == part[-1] and part[0] in "\"'": resolved.append(part[1:-1]) elif part in env: resolved.append(str(env[part])) else: return None return "".join(resolved) def _lua_unquote(s: str) -> str: s = s.strip() if s.startswith("[[") and s.endswith("]]"): return s[2:-2] if len(s) >= 2 and s[0] == s[-1] and s[0] in "\"'": return s[1:-1] return s def _lua_dispatch_desc(call_expr: str): m = re.match(r'^hl\.dsp\.([\w.]+)\((.*)\)$', call_expr.strip(), re.DOTALL) if not m: return None dispatcher, arg_str = m.group(1), m.group(2) if dispatcher == "exec_cmd": return _exec_desc(_lua_unquote(arg_str)) if dispatcher == "window.kill": return "Aktív ablak bezárása" if dispatcher == "window.float": return "Lebegő mód ki/be" if dispatcher == "window.drag": return "Ablak húzása egérrel" if dispatcher == "window.resize": return "Ablak átméretezése egérrel" if dispatcher == "focus": dm = re.search(r'direction\s*=\s*"(\w+)"', arg_str) if dm: names = {"left": "balra", "right": "jobbra", "up": "fel", "down": "le"} return f"Fókusz mozgatása {names.get(dm.group(1), dm.group(1))}" wm = re.search(r'workspace\s*=\s*(-?\d+)', arg_str) if wm: return f"Váltás {wm.group(1)}. munkaterületre" return "Fókusz mozgatása" if dispatcher == "window.move": wm = re.search(r'workspace\s*=\s*(-?\d+)', arg_str) if wm: return f"Ablak áthelyezése {wm.group(1)}. munkaterületre" return "Ablak áthelyezése" return None def _lua_simple_arith(expr: str, env: dict): expr = expr.strip() m = re.match(r'^(\w+)\s*%\s*(\d+)$', expr) if m and m.group(1) in env: try: return int(env[m.group(1)]) % int(m.group(2)) except (TypeError, ValueError): return None m = re.match(r'^(\w+)\s*([+-])\s*(\d+)$', expr) if m and m.group(1) in env: try: base = int(env[m.group(1)]) return base + int(m.group(3)) if m.group(2) == "+" else base - int(m.group(3)) except (TypeError, ValueError): return None if expr.lstrip("-").isdigit(): return int(expr) return env.get(expr) def _lua_try_add_bind(line: str, env: dict, binds: list): inner = _lua_extract_call(line, "hl.bind") if inner is None: return args = _lua_split_args(inner) if len(args) < 2: return if "XF86" in args[0]: return combo_raw = _lua_resolve_key_expr(args[0], env) if combo_raw is None: return combo = _fmt_key(combo_raw) # ciklus-valtozok (pl. workspace = i) behelyettesitese a dispatcher # kifejezesbe is, hogy a leiras a tenyleges munkaterulet-szamot mutassa dispatch_expr = args[1] for name, value in env.items(): if isinstance(value, int): dispatch_expr = re.sub(rf'\b{re.escape(name)}\b', str(value), dispatch_expr) desc = _lua_dispatch_desc(dispatch_expr) if combo and desc: binds.append((combo, desc)) def _parse_hypr_binds_lua(path: Path) -> list: if not path.exists(): return [] try: lines = path.read_text(errors="replace").splitlines() except Exception: return [] env = {} binds = [] i = 0 n = len(lines) while i < n: line = lines[i].strip() if not line or line.startswith("--"): i += 1 continue m = re.match(r'^local\s+(\w+)\s*=\s*"([^"]*)"\s*$', line) if m: env[m.group(1)] = m.group(2) i += 1 continue m = re.match(r'^for\s+(\w+)\s*=\s*(-?\d+)\s*,\s*(-?\d+)\s+do\s*$', line) if m: var, start, end = m.group(1), int(m.group(2)), int(m.group(3)) body = [] depth = 1 j = i + 1 while j < n and depth > 0: bline = lines[j].strip() if bline == "end": depth -= 1 if depth == 0: break elif re.match(r'^(for|if|function)\b', bline) and \ (bline.endswith("do") or bline.endswith("then")): depth += 1 if depth > 0: body.append(bline) j += 1 for val in range(start, end + 1): loop_env = dict(env) loop_env[var] = val for bline in body: lm = re.match(r'^local\s+(\w+)\s*=\s*(.+)$', bline) if lm: result = _lua_simple_arith(lm.group(2), loop_env) if result is not None: loop_env[lm.group(1)] = result continue _lua_try_add_bind(bline, loop_env, binds) i = j + 1 continue _lua_try_add_bind(line, env, binds) i += 1 return binds # ─── Page: Hyprland Keybinds ───────────────────────────────────────────────── class HyprlandPage(QWidget): def __init__(self): super().__init__() cfg = HYPR_CONFIG if HYPR_CONFIG.exists() else _HYPR_FALLBACK self._binds = _parse_hypr_binds(cfg) self._cfg_path = cfg if not self._binds: # nincs hagyomanyos hyprland.conf (vagy nincs benne bind sor) - # a RaveOS Hyprland 0.55+ Lua config modot hasznalja lua_cfg = HYPR_KEYBINDS_LUA if HYPR_KEYBINDS_LUA.exists() else _HYPR_KEYBINDS_LUA_FALLBACK lua_binds = _parse_hypr_binds_lua(lua_cfg) if lua_binds: self._binds = lua_binds self._cfg_path = lua_cfg self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 28, 60, 20) lay.setSpacing(10) header = QHBoxLayout() self.title = QLabel(_t("hypr_title")) self.title.setObjectName("section_title") header.addWidget(self.title) header.addStretch() self.src = QLabel(_t("hypr_source") + str(self._cfg_path)) self.src.setStyleSheet(f"font-size: 10px; color: {COLORS['text_dim']};") header.addWidget(self.src) lay.addLayout(header) if not self._binds: self.msg = QLabel(_t("hypr_none")) self.msg.setObjectName("body") self.msg.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.msg, alignment=Qt.AlignmentFlag.AlignCenter) lay.addStretch() return scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setStyleSheet("background: transparent;") inner = QWidget() inner.setStyleSheet("background: transparent;") vlay = QVBoxLayout(inner) vlay.setContentsMargins(0, 4, 8, 0) vlay.setSpacing(0) card = QFrame() card.setObjectName("card") card_lay = QVBoxLayout(card) card_lay.setContentsMargins(16, 10, 16, 10) card_lay.setSpacing(0) for i, (key, desc) in enumerate(self._binds): row = QHBoxLayout() row.setContentsMargins(0, 6, 0, 6) row.setSpacing(12) key_lbl = QLabel(key) key_lbl.setFixedWidth(230) key_lbl.setStyleSheet( f"font-family: monospace; font-size: 12px; font-weight: 600;" f" color: {COLORS['accent_light']}; background: {COLORS['surface2']};" f" border-radius: 4px; padding: 3px 8px;" ) desc_lbl = QLabel(desc) desc_lbl.setStyleSheet(f"font-size: 13px; color: {COLORS['text']};") row.addWidget(key_lbl) row.addWidget(desc_lbl) row.addStretch() card_lay.addLayout(row) if i < len(self._binds) - 1: div = QFrame() div.setFixedHeight(1) div.setStyleSheet(f"background: {COLORS['border']}; border: none;") card_lay.addWidget(div) vlay.addWidget(card) vlay.addStretch() scroll.setWidget(inner) lay.addWidget(scroll, 1) def retranslate(self): self.title.setText(_t("hypr_title")) self.src.setText(_t("hypr_source") + str(self._cfg_path)) if not self._binds: self.msg.setText(_t("hypr_none")) # ─── GPU Detection ──────────────────────────────────────────────────────────── def _detect_gpu() -> str: try: out = subprocess.check_output( ["lspci"], stderr=subprocess.DEVNULL ).decode().lower() if "amd" in out or "radeon" in out: return "amd" if "nvidia" in out: return "nvidia" except Exception: pass return "other" # ─── Page 3: Optimize ────────────────────────────────────────────────────────── OPTIMIZE_SCRIPT = "/usr/share/raveos-welcome/gaming-optimize.sh" OPTIMIZE_LOG = "/var/log/raveos-welcome/gaming-optimize.log" CHECKBOX_STYLE = f""" QCheckBox {{ color: {COLORS['text']}; font-size: 13px; spacing: 8px; }} QCheckBox::indicator {{ width: 16px; height: 16px; border-radius: 3px; border: 1px solid {COLORS['border']}; background: {COLORS['surface2']}; }} QCheckBox::indicator:checked {{ background: {COLORS['accent']}; border-color: {COLORS['accent']}; }} """ class OptimizePage(QWidget): def __init__(self): super().__init__() self._gpu = _detect_gpu() self._checks = {} self._hints = {} self._proc = None self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 28, 60, 20) lay.setSpacing(10) self.title = QLabel(_t("opt_title")) self.title.setObjectName("section_title") lay.addWidget(self.title) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setStyleSheet("background: transparent;") inner = QWidget() inner.setStyleSheet("background: transparent;") vlay = QVBoxLayout(inner) vlay.setContentsMargins(0, 0, 8, 0) vlay.setSpacing(4) # --- Browsers --- self.sec1 = QLabel(_t("opt_browser")) self.sec1.setObjectName("section_title") vlay.addWidget(self.sec1) vlay.addSpacing(4) browsers = [ ("brave", "opt_brave", "opt_brave_desc"), ("firefox", "opt_firefox", "opt_firefox_desc"), ] self._browser_checks = {} for key, label_key, desc_key in browsers: cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=False) self._browser_checks[key] = (label_key, desc_key, cb) cb_bp = self._add_check(vlay, "brave_profile", _t("opt_brave_profile"), _t("opt_brave_profile_desc"), default=False) self._brave_profile_check = ("opt_brave_profile", "opt_brave_profile_desc", cb_bp) vlay.addSpacing(14) # --- Gaming optimization --- gpu_name = {"amd": "AMD", "nvidia": "Nvidia"}.get(self._gpu, "Ismeretlen") self.sec2 = QLabel(_t("opt_gaming", gpu=gpu_name)) self.sec2.setObjectName("section_title") vlay.addWidget(self.sec2) vlay.addSpacing(4) common = [ ("sysctl", "opt_sysctl", "opt_sysctl_desc"), ("io_sched", "opt_io_sched", "opt_io_sched_desc"), ("ananicy", "opt_ananicy", "opt_ananicy_desc"), ("gamemode", "opt_gamemode", "opt_gamemode_desc"), ] self._common_checks = {} for key, label_key, desc_key in common: cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=True) self._common_checks[key] = (label_key, desc_key, cb) self._amd_checks = {} self._nvidia_checks = {} if self._gpu == "amd": amd_opts = [ ("gpu_profile", "opt_gpu_profile", "opt_gpu_profile_desc"), ("amd_overdrive", "opt_amd_overdrive", "opt_amd_overdrive_desc"), ("amd_powercap", "opt_amd_powercap", "opt_amd_powercap_desc"), ] for key, label_key, desc_key in amd_opts: cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=(key in ("gpu_profile", "amd_powercap"))) self._amd_checks[key] = (label_key, desc_key, cb) elif self._gpu == "nvidia": cb = self._add_check(vlay, "nvidia_perf", _t("opt_nvidia_perf"), _t("opt_nvidia_perf_desc"), default=True) self._nvidia_checks["nvidia_perf"] = ("opt_nvidia_perf", "opt_nvidia_perf_desc", cb) vlay.addStretch() scroll.setWidget(inner) lay.addWidget(scroll, 1) # Status + gomb self.status_lbl = QLabel("") self.status_lbl.setObjectName("body") self.status_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.status_lbl) self.progress = QProgressBar() self.progress.setRange(0, 0) self.progress.setFixedHeight(4) self.progress.setVisible(False) lay.addWidget(self.progress) self.apply_btn = QPushButton(_t("opt_apply")) self.apply_btn.setObjectName("primary") self.apply_btn.clicked.connect(self._apply) lay.addWidget(self.apply_btn, alignment=Qt.AlignmentFlag.AlignRight) def retranslate(self): self.title.setText(_t("opt_title")) self.sec1.setText(_t("opt_browser")) gpu_name = {"amd": "AMD", "nvidia": "Nvidia"}.get(self._gpu, "Ismeretlen") self.sec2.setText(_t("opt_gaming", gpu=gpu_name)) self.apply_btn.setText(_t("opt_apply")) for key, (lk, dk, cb) in self._browser_checks.items(): cb.setText(_t(lk)) if key in self._hints: self._hints[key].setToolTip(_t(dk)) bp_lk, bp_dk, bp_cb = self._brave_profile_check bp_cb.setText(_t(bp_lk)) if "brave_profile" in self._hints: self._hints["brave_profile"].setToolTip(_t(bp_dk)) for key, (lk, dk, cb) in self._common_checks.items(): cb.setText(_t(lk)) if key in self._hints: self._hints[key].setToolTip(_t(dk)) for key, (lk, dk, cb) in self._amd_checks.items(): cb.setText(_t(lk)) if key in self._hints: self._hints[key].setToolTip(_t(dk)) for key, (lk, dk, cb) in self._nvidia_checks.items(): cb.setText(_t(lk)) if key in self._hints: self._hints[key].setToolTip(_t(dk)) def _add_check(self, layout, key, label, desc, default): row = QHBoxLayout() row.setContentsMargins(0, 0, 0, 0) row.setSpacing(6) cb = QCheckBox(label) cb.setChecked(default) cb.setStyleSheet(CHECKBOX_STYLE) row.addWidget(cb) hint = QLabel("?") hint.setToolTip(desc) hint.setCursor(Qt.CursorShape.WhatsThisCursor) hint.setFixedSize(16, 16) hint.setAlignment(Qt.AlignmentFlag.AlignCenter) hint.setStyleSheet( f"color: {COLORS['text_dim']}; font-size: 11px; font-weight: 600;" f" background: {COLORS['surface2']}; border-radius: 8px;" f" border: 1px solid {COLORS['border']};" ) row.addWidget(hint) row.addStretch() layout.addLayout(row) layout.addSpacing(3) self._checks[key] = cb self._hints[key] = hint return cb def _apply(self): selected = [k for k, cb in self._checks.items() if cb.isChecked()] if not selected: self.status_lbl.setStyleSheet(f"color: {COLORS['warning']};") self.status_lbl.setText(_t("opt_apply_empty")) return script = OPTIMIZE_SCRIPT if not Path(script).exists(): self.status_lbl.setStyleSheet(f"color: {COLORS['error']};") self.status_lbl.setText(_t("opt_apply_missing") + script) return self.apply_btn.setEnabled(False) self.status_lbl.setText("") self.progress.setVisible(True) self._proc = QProcess(self) self._proc.finished.connect(self._on_done) self._proc.start("pkexec", [script] + selected) def _on_done(self, exit_code, _): self.apply_btn.setEnabled(True) self.progress.setVisible(False) if exit_code == 0: self.status_lbl.setStyleSheet(f"color: {COLORS['success']};") self.status_lbl.setText(_t("opt_apply_ok")) else: self.status_lbl.setStyleSheet(f"color: {COLORS['error']};") self.status_lbl.setText(_t("opt_apply_err", code=exit_code, log=OPTIMIZE_LOG)) # ─── Page 4: Credits ────────────────────────────────────────────────────────── class CreditsPage(QWidget): def __init__(self): super().__init__() self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 40, 60, 40) lay.setSpacing(0) self.title = QLabel(_t("credits_title")) self.title.setObjectName("section_title") self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.title) sep = QFrame() sep.setObjectName("separator") sep.setFixedHeight(1) lay.addSpacing(16) lay.addWidget(sep) lay.addSpacing(24) people = [ ("credits_rp1", "credits_rp1_name", "credits_rp1_role", "credits_rp1_desc"), ("credits_alexc", "credits_alexc_name", "credits_alexc_role", "credits_alexc_desc"), ("credits_nippy", "credits_nippy_name", "credits_nippy_role", "credits_nippy_desc"), ("credits_gabesz","credits_gabesz_name","credits_gabesz_role","credits_gabesz_desc"), ("credits_stefi", "credits_stefi_name", "credits_stefi_role", "credits_stefi_desc"), ] self._people_cards = [] for prefix, name_key, role_key, desc_key in people: card = QFrame() card.setObjectName("card") card_lay = QHBoxLayout(card) card_lay.setContentsMargins(20, 14, 20, 14) card_lay.setSpacing(16) left = QVBoxLayout() left.setSpacing(2) name_lbl = QLabel(_t(name_key)) name_lbl.setStyleSheet( f"font-size: 15px; font-weight: 700; color: {COLORS['text']};" ) role_lbl = QLabel(_t(role_key)) role_lbl.setStyleSheet( f"font-size: 11px; font-weight: 600; letter-spacing: 1px;" f" color: {COLORS['accent_light']};" ) left.addWidget(name_lbl) left.addWidget(role_lbl) desc_lbl = QLabel(_t(desc_key)) desc_lbl.setStyleSheet( f"font-size: 12px; color: {COLORS['text_dim']}; font-style: italic;" ) desc_lbl.setWordWrap(True) desc_lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) card_lay.addLayout(left) card_lay.addStretch() card_lay.addWidget(desc_lbl) lay.addWidget(card) lay.addSpacing(10) self._people_cards.append((name_key, role_key, desc_key, name_lbl, role_lbl, desc_lbl)) lay.addStretch() def retranslate(self): self.title.setText(_t("credits_title")) for name_key, role_key, desc_key, name_lbl, role_lbl, desc_lbl in self._people_cards: name_lbl.setText(_t(name_key)) role_lbl.setText(_t(role_key)) desc_lbl.setText(_t(desc_key)) # ─── Page 5: Finish ──────────────────────────────────────────────────────────── class FinishPage(QWidget): launch_installer = pyqtSignal() def __init__(self): super().__init__() self._build() def _build(self): lay = QVBoxLayout(self) lay.setContentsMargins(60, 40, 60, 40) lay.setSpacing(0) self.done_label = QLabel(_t("finish_ok")) self.done_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.done_label.setStyleSheet( f"font-size: 52px; font-weight: 700; color: {COLORS['success']};" f"font-family: Ubuntu; padding: 10px;" ) lay.addWidget(self.done_label) lay.addSpacing(12) self.title = QLabel(_t("finish_title")) self.title.setObjectName("title") self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.title) lay.addSpacing(6) self.sub = QLabel(_t("finish_sub")) self.sub.setObjectName("subtitle") self.sub.setAlignment(Qt.AlignmentFlag.AlignCenter) lay.addWidget(self.sub) sep = QFrame() sep.setObjectName("separator") sep.setFixedHeight(1) lay.addSpacing(28) lay.addWidget(sep) lay.addSpacing(24) # App installer CTA card card = QFrame() card.setObjectName("card") card_lay = QVBoxLayout(card) card_lay.setContentsMargins(24, 18, 24, 18) card_lay.setSpacing(10) self.card_title = QLabel(_t("finish_next_step")) self.card_title.setObjectName("section_title") card_lay.addWidget(self.card_title) self.card_body = QLabel(_t("finish_inst_desc")) self.card_body.setObjectName("body") self.card_body.setWordWrap(True) card_lay.addWidget(self.card_body) self.install_btn = QPushButton(_t("finish_install")) self.install_btn.setObjectName("primary") self.install_btn.clicked.connect(self.launch_installer.emit) card_lay.addWidget(self.install_btn, alignment=Qt.AlignmentFlag.AlignRight) self.install_status = QLabel("") self.install_status.setObjectName("body") self.install_status.setWordWrap(True) self.install_status.setVisible(False) card_lay.addWidget(self.install_status) lay.addWidget(card) lay.addStretch() def retranslate(self): self.done_label.setText(_t("finish_ok")) self.title.setText(_t("finish_title")) self.sub.setText(_t("finish_sub")) self.card_title.setText(_t("finish_next_step")) self.card_body.setText(_t("finish_inst_desc")) self.install_btn.setText(_t("finish_install")) def show_error(self, msg): self.install_btn.setEnabled(True) self.install_status.setStyleSheet(f"color: {COLORS['error']};") self.install_status.setText(msg) self.install_status.setVisible(True) # ─── Main Window ─────────────────────────────────────────────────────────────── class RaveOSWelcome(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("RaveOS — Üdvözlünk") self.setMinimumSize(720, 640) self.resize(800, 720) self.setStyleSheet(STYLESHEET) self._show_wifi = _should_show_wifi() self._show_hyprland = _detect_hyprland() central = QWidget() central.setObjectName("central") self.setCentralWidget(central) root = QVBoxLayout(central) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # Step indicator bar self.step_bar = self._make_step_bar() root.addWidget(self.step_bar) # Page container self.stack = QStackedWidget() root.addWidget(self.stack, stretch=1) # Pages self.p_welcome = WelcomePage() self.p_optimize = OptimizePage() self.p_credits = CreditsPage() self.p_finish = FinishPage() self.stack.addWidget(self.p_welcome) if self._show_wifi: self.p_wifi = WiFiPage() self.stack.addWidget(self.p_wifi) self.stack.addWidget(self.p_optimize) if self._show_hyprland: self.p_hyprland = HyprlandPage() self.stack.addWidget(self.p_hyprland) self.stack.addWidget(self.p_credits) self.stack.addWidget(self.p_finish) # Nav buttons nav = QHBoxLayout() nav.setContentsMargins(24, 10, 24, 18) nav.setSpacing(12) # Language selector (bottom-left) self.lang_hu_btn = QPushButton("Magyar") self.lang_hu_btn.setObjectName("lang_btn") self.lang_hu_btn.setFixedHeight(30) self.lang_hu_btn.setCheckable(True) self.lang_hu_btn.setChecked(True) self.lang_hu_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;" f" border: none; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}" ) self.lang_hu_btn.clicked.connect(lambda: self._set_lang("hu")) nav.addWidget(self.lang_hu_btn) self.lang_en_btn = QPushButton("English") self.lang_en_btn.setObjectName("lang_btn") self.lang_en_btn.setFixedHeight(30) self.lang_en_btn.setCheckable(True) self.lang_en_btn.setChecked(False) self.lang_en_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};" f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}" ) self.lang_en_btn.clicked.connect(lambda: self._set_lang("en")) nav.addWidget(self.lang_en_btn) self.back_btn = QPushButton(_t("nav_back")) self.back_btn.setObjectName("secondary") self.back_btn.clicked.connect(self._prev_page) self.back_btn.setVisible(False) nav.addWidget(self.back_btn) nav.addStretch() self.next_btn = QPushButton(_t("nav_next")) self.next_btn.setObjectName("primary") self.next_btn.clicked.connect(self._next_or_close) nav.addWidget(self.next_btn) root.addLayout(nav) self.p_finish.launch_installer.connect(self._launch_installer) self._update_nav() def _set_lang(self, lang): global _current_lang if _current_lang == lang: return _current_lang = lang self.lang_hu_btn.setChecked(lang == "hu") self.lang_en_btn.setChecked(lang == "en") if lang == "hu": self.lang_hu_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;" f" border: none; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}" ) self.lang_en_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};" f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}" ) else: self.lang_en_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;" f" border: none; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}" ) self.lang_hu_btn.setStyleSheet( f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};" f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;" f" font-size: 11px; font-weight: 600; }}" f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}" f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}" ) self._retranslate_all() def _make_step_bar(self): bar = QWidget() bar.setFixedHeight(48) bar.setStyleSheet( f"background: {COLORS['bg2']}; border-bottom: 1px solid {COLORS['border']};" ) lay = QHBoxLayout(bar) lay.setContentsMargins(24, 0, 24, 0) self.step_labels = [] self._step_keys = ["step_welcome"] if self._show_wifi: self._step_keys.append("step_network") self._step_keys.append("step_optimize") if self._show_hyprland: self._step_keys.append("step_hyprland") self._step_keys += ["step_credits", "step_finish"] for i, key in enumerate(self._step_keys): if i > 0: dot = QLabel("·····") dot.setStyleSheet(f"color: {COLORS['border']}; font-size: 10px; letter-spacing: 2px;") lay.addWidget(dot) lbl = QLabel(f"{i+1} {_t(key).upper()}") lbl.setStyleSheet( f"font-size: 11px; font-weight: 600; letter-spacing: 2px; " f"color: {COLORS['text_dim']};" ) lay.addWidget(lbl) self.step_labels.append(lbl) lay.addStretch() return bar def _update_nav(self): idx = self.stack.currentIndex() self.back_btn.setVisible(idx > 0) is_last = idx == self.stack.count() - 1 self.next_btn.setText(_t("finish_close") if is_last else _t("nav_next")) self.lang_hu_btn.setVisible(idx == 0) self.lang_en_btn.setVisible(idx == 0) self._update_steps(idx) def _update_steps(self, idx): for i, lbl in enumerate(self.step_labels): if i < idx: lbl.setStyleSheet( f"font-size: 11px; font-weight: 600; letter-spacing: 2px; " f"color: {COLORS['success']};" ) elif i == idx: lbl.setStyleSheet( f"font-size: 11px; font-weight: 700; letter-spacing: 2px; " f"color: {COLORS['accent_light']};" ) else: lbl.setStyleSheet( f"font-size: 11px; font-weight: 600; letter-spacing: 2px; " f"color: {COLORS['text_dim']};" ) def _retranslate_all(self): if _current_lang == "en": self.setWindowTitle("RaveOS — Welcome") else: self.setWindowTitle("RaveOS — Üdvözlünk") self.back_btn.setText(_t("nav_back")) is_last = self.stack.currentIndex() == self.stack.count() - 1 self.next_btn.setText(_t("finish_close") if is_last else _t("nav_next")) for i, key in enumerate(self._step_keys): self.step_labels[i].setText(f"{i+1} {_t(key).upper()}") self.p_welcome.retranslate() if self._show_wifi: self.p_wifi.retranslate() self.p_optimize.retranslate() if self._show_hyprland: self.p_hyprland.retranslate() self.p_credits.retranslate() self.p_finish.retranslate() def _next_or_close(self): idx = self.stack.currentIndex() if idx == self.stack.count() - 1: QApplication.quit() else: self._next_page() def _next_page(self): idx = self.stack.currentIndex() if idx < self.stack.count() - 1: self.stack.setCurrentIndex(idx + 1) self._update_nav() def _prev_page(self): idx = self.stack.currentIndex() if idx > 0: self.stack.setCurrentIndex(idx - 1) self._update_nav() def _launch_installer(self): candidates = [ APP_INSTALLER, shutil.which("raveos-app-installer") or "", ] cmd = None for c in candidates: if c and Path(c).exists(): cmd = c break if not cmd: self.p_finish.show_error(_t("finish_install_missing")) return self.p_finish.install_btn.setEnabled(False) log_path = Path.home() / ".config" / "raveos" / "app-installer.log" log_path.parent.mkdir(parents=True, exist_ok=True) is_hyprland = bool(os.environ.get("HYPRLAND_INSTANCE_SIGNATURE")) or \ os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == "hyprland" if is_hyprland: # Hyprland 0.55+ Lua config mod alatt a hagyomanyos # "hyprctl dispatch exec " szintaxis mar nem mukodik, # csak a hl.dsp.exec_cmd("...") Lua hivas - de a regi .conf # alapu Hyprland telepiteseken meg a regi szintaxis a jo, ezert # eloszor azt probaljuk, es csak hiba eseten valtunk Lua-ra. result = subprocess.run( ["hyprctl", "dispatch", "exec", cmd], capture_output=True, text=True ) if "error" in (result.stdout + result.stderr).lower(): escaped = cmd.replace('"', '\\"') subprocess.run( ["hyprctl", "dispatch", f'hl.dsp.exec_cmd("{escaped}")'], capture_output=True, text=True ) _write_flag() self.p_finish.install_btn.setEnabled(True) return try: with open(log_path, "ab") as log: proc = subprocess.Popen( [cmd], stdout=log, stderr=log, start_new_session=True ) except OSError as e: self.p_finish.show_error(_t("finish_install_err", log=str(e))) return _write_flag() def _check(): code = proc.poll() if code is not None and code != 0: self.p_finish.show_error(_t("finish_install_err", log=str(log_path))) else: self.p_finish.install_btn.setEnabled(True) QTimer.singleShot(1200, _check) # ─── Flag helpers ────────────────────────────────────────────────────────────── def _flag_exists() -> bool: return FLAG_FILE.exists() def _write_flag(): FLAG_FILE.parent.mkdir(parents=True, exist_ok=True) FLAG_FILE.touch() # ─── Fast tooltip style ──────────────────────────────────────────────────────── class _FastTooltipStyle(QProxyStyle): def styleHint(self, hint, option=None, widget=None, returnData=None): if hint == QStyle.StyleHint.SH_ToolTip_WakeUpDelay: return 100 if hint == QStyle.StyleHint.SH_ToolTip_FallAsleepDelay: return 5000 return super().styleHint(hint, option, widget, returnData) # ─── Entry point ─────────────────────────────────────────────────────────────── def main(): if _flag_exists() and "--force" not in sys.argv: print("RaveOS Welcome: már futott, kihagyva. (--force a felülíráshoz)") sys.exit(0) app = QApplication(sys.argv) app.setStyle(_FastTooltipStyle()) app.setApplicationName("raveos-welcome-session") app.setDesktopFileName("raveos-welcome") QFontDatabase.addApplicationFont("/usr/share/fonts/ubuntu/Ubuntu-R.ttf") QFontDatabase.addApplicationFont("/usr/share/fonts/ubuntu/Ubuntu-B.ttf") app_icon = QIcon() svg_path = _find_svg() if svg_path: from PyQt6.QtSvg import QSvgRenderer from PyQt6.QtGui import QPixmap, QPainter renderer = QSvgRenderer(svg_path) for size in (16, 32, 48, 64, 128, 256, 512): px = QPixmap(size, size) px.fill(Qt.GlobalColor.transparent) p = QPainter(px) renderer.render(p) p.end() app_icon.addPixmap(px) else: app_icon = QIcon.fromTheme("raveos-welcome") app.setWindowIcon(app_icon) window = RaveOSWelcome() window.setWindowIcon(app_icon) window.show() ret = app.exec() _write_flag() sys.exit(ret) if __name__ == "__main__": main()