diff options
| author | Nippy <nippy@Nippy-Mac-mini.local> | 2026-11-01 18:13:54 +0100 |
|---|---|---|
| committer | Nippy <nippy@Nippy-Mac-mini.local> | 2026-11-01 18:13:54 +0100 |
| commit | a64ef966534f8dba6af889ba4bfce15691bfc85b (patch) | |
| tree | b30f284d05b80fa115569ab220deae13797fad6f /raveos-app-installer/raveos-app-installer.py | |
| parent | 919f977b40016f588b0c8532c8a57f6035056b95 (diff) | |
| download | RaveOS-PKGBUILD-a64ef966534f8dba6af889ba4bfce15691bfc85b.tar.gz RaveOS-PKGBUILD-a64ef966534f8dba6af889ba4bfce15691bfc85b.zip | |
új telepítő
Diffstat (limited to 'raveos-app-installer/raveos-app-installer.py')
| -rw-r--r-- | raveos-app-installer/raveos-app-installer.py | 1870 |
1 files changed, 0 insertions, 1870 deletions
diff --git a/raveos-app-installer/raveos-app-installer.py b/raveos-app-installer/raveos-app-installer.py deleted file mode 100644 index ab841f3..0000000 --- a/raveos-app-installer/raveos-app-installer.py +++ /dev/null @@ -1,1870 +0,0 @@ -#!/usr/bin/env python3 -""" -RaveOS App Installer -==================== -Egyetlen fájlos, letisztult PyQt6 alkalmazástelepítő a RaveOS-hez. - -Tervezési elvek: - * A welcome app vizuális témája: dark charcoal, muted natural green accent, - Bebas Neue display font, minimal lekerekítés, semmi neon / cyberpunk. - * USER-ként fut (NEM root). A rendszerszintű parancsok (pacman) per-művelet - `pkexec`-en mennek -> Wayland/Hyprland-helyes (a root GUI Waylanden gáz). - * Háttér QThread a telepítéshez -> a GUI nem fagy be / nem zár be magától. - * GUI + CLI ugyanabból a fájlból (argparse). - * Az alkalmazás-katalógus külön `app-list.json`-ban (te frissíted GitHubról). - -Backendek (app "type" mezője): - pacman -> pkexec pacman -S ... (id = csomagnév) - aur -> yay -S ... user-ként (id = AUR csomagnév) - flatpak -> flatpak install --user ... (id = flatpak app id, flathubról) - script -> bash -c <id> (id = a futtatandó parancs) - -Függőségek: python-pyqt6, polkit (pkexec), flatpak, opcionálisan yay. -Hyprlandon kell egy polkit agent is (pl. hyprpolkit-agent vagy polkit-gnome). -""" - -import argparse -import json -import os -import shutil -import subprocess -import sys -from pathlib import Path - - -APP_NAME = "RaveOS App Installer" -APP_ID = "raveos-app-installer" -APP_VERSION = "3.18" - -_SCRIPT_DIR = Path(__file__).resolve().parent -APP_LIST_CANDIDATES = [ - _SCRIPT_DIR / "app-list.json", - Path("/usr/share/raveos/app-list.json"), - Path.home() / ".config" / "raveos" / "app-list.json", -] - - -C = { - "bg": "#232427", - "header": "#1b1c1e", - "surface": "#2b2c30", - "surface_hi": "#34353a", - "border": "#3a3b40", - "border_hi": "#4a4b52", - "accent": "#62a052", - "accent_hi": "#72b062", - "accent_dim": "#4d8043", - "text": "#eaeaea", - "text_dim": "#9298a0", - "text_mut": "#6c727a", - "danger": "#b5544a", - "danger_hi": "#c56458", -} - -TYPE_LABEL = { - "pacman": "pacman", - "aur": "AUR", - "flatpak": "flatpak", - "flatpak-user": "flatpak", - "script": "script", - "message": "info", - "choice": "menu", -} - - - -_current_lang = "hu" - -T = { - "header_installer": {"hu": " · ALKALMAZÁSTELEPÍTŐ", "en": " · APP INSTALLER"}, - "header_backup": {"hu": " · MENTÉS / VISSZAÁLLÍTÁS", "en": " · BACKUP / RESTORE"}, - "nav_backup": {"hu": "Mentés / Visszaállítás", "en": "Backup / Restore"}, - "nav_back": {"hu": "← Vissza", "en": "← Back"}, - "nav_sb": {"hu": "Secure Boot", "en": "Secure Boot"}, - "header_sb": {"hu": " · SECURE BOOT", "en": " · SECURE BOOT"}, - "sb_desc": {"hu": "A telepített rendszer aláírása és a kulcsok telepítése a firmware-be.\nWindows dual-boot mellett is biztonságos (Microsoft kulcsok megőrizve).", "en": "Sign the installed system and enroll the keys into the firmware.\nSafe with dual-boot Windows (Microsoft keys preserved)."}, - "sb_beta": {"hu": "Ez egy béta funkció. A használat saját felelősségre történik.", "en": "This is a beta feature. Use at your own risk."}, - "sb_lbl_sb": {"hu": "Secure Boot", "en": "Secure Boot"}, - "sb_lbl_setup": {"hu": "Setup Mode", "en": "Setup Mode"}, - "sb_lbl_keys": {"hu": "Gépkulcs", "en": "Machine key"}, - "sb_lbl_signed": {"hu": "Aláírások", "en": "Signatures"}, - "sb_val_on": {"hu": "bekapcsolva", "en": "enabled"}, - "sb_val_off": {"hu": "kikapcsolva", "en": "disabled"}, - "sb_val_yes": {"hu": "igen", "en": "yes"}, - "sb_val_no": {"hu": "nem", "en": "no"}, - "sb_val_ok": {"hu": "rendben", "en": "ok"}, - "sb_val_missing": {"hu": "hiányzik", "en": "missing"}, - "sb_btn_refresh": {"hu": "Állapot frissítése", "en": "Refresh status"}, - "sb_btn_setup": {"hu": "Secure Boot beállítása", "en": "Set up Secure Boot"}, - "sb_btn_resign": {"hu": "Újra-aláírás", "en": "Re-sign"}, - "sb_pending": {"hu": "Újraindítás szükséges a befejezéshez!", "en": "Reboot required to finish!"}, - "sb_bl_warn": {"hu": "BitLocker: ha Windows-on be van kapcsolva, előbb indítsd el a Windowst és futtasd: manage-bde -protectors -disable C: -rebootcount 2, vagy legyen kéznél a helyreállítási kulcs.", "en": "BitLocker: if enabled on Windows, boot Windows first and run: manage-bde -protectors -disable C: -rebootcount 2, or have the recovery key ready."}, - "sb_done_title": {"hu": "Secure Boot beállítva", "en": "Secure Boot configured"}, - "sb_done_msg": {"hu": "Az aláírások és a kulcsok készen vannak.\n\nA BIOS-ban állítsd a Secure Boot-ot Enabled-re, majd indítsd újra a gépet.", "en": "Signatures and keys are ready.\n\nIn the BIOS set Secure Boot to Enabled, then reboot."}, - "sb_reboot_title": {"hu": "Újraindítás szükséges", "en": "Reboot required"}, - "sb_reboot_msg": {"hu": "A BIOS-ban töröld a Secure Boot kulcsokat (Clear Secure Boot keys, ezzel a gép Setup Mode-ba kerül).\nMentés után indítsd újra a gépet. A folyamat boot után automatikusan befejeződik.", "en": "In the BIOS clear the Secure Boot keys (Clear Secure Boot keys, the machine enters Setup Mode).\nSave and reboot. The setup will finish automatically after boot."}, - "sb_btn_reboot": {"hu": "Újraindítás", "en": "Restart"}, - "sb_btn_cancel": {"hu": "Mégse", "en": "Cancel"}, - "sb_err_title": {"hu": "Secure Boot hiba", "en": "Secure Boot error"}, - "sb_err_msg": {"hu": "A művelet nem sikerült, a részletek a naplóban láthatók.", "en": "The operation failed, see the log for details."}, - "tab_all": {"hu": "Összes", "en": "All"}, - "tab_installed": {"hu": "Telepített", "en": "Installed"}, - "search_placeholder": {"hu": "Keresés…", "en": "Search…"}, - "btn_install": {"hu": "Telepítés", "en": "Install"}, - "btn_remove": {"hu": "Eltávolítás", "en": "Remove"}, - "row_installed": {"hu": "telepítve", "en": "installed"}, - "row_not_installed": {"hu": "nincs telepítve", "en": "not installed"}, - "row_info": {"hu": "info", "en": "info"}, - "choice_installed": {"hu": "{n}/{total} telepítve", "en": "{n}/{total} installed"}, - "choice_pick": {"hu": "Válassz appot…", "en": "Choose apps…"}, - "choice_selected": {"hu": "{n} kiválasztva", "en": "{n} selected"}, - "dest_placeholder": {"hu": "Backup fájl útvonala (.tar.gz)…", "en": "Backup file path (.tar.gz)…"}, - "btn_browse": {"hu": "Tallóz", "en": "Browse"}, - "btn_save": {"hu": "Mentés", "en": "Save"}, - "btn_restore": {"hu": "Visszaállítás", "en": "Restore"}, - "dlg_auth_title": {"hu": "Hitelesítés szükséges", "en": "Authentication required"}, - "dlg_pw_label": {"hu": "Sudo jelszó:", "en": "Sudo password:"}, - "dlg_pw_placeholder": {"hu": "Jelszó…", "en": "Password…"}, - "dlg_pw_empty_err": {"hu": "A jelszó nem lehet üres.", "en": "Password cannot be empty."}, - "dlg_pw_wrong_err": {"hu": "Hibás jelszó, próbáld újra.", "en": "Wrong password, try again."}, - "dlg_ok": {"hu": "OK", "en": "OK"}, - "dlg_cancel": {"hu": "Mégse", "en": "Cancel"}, - "log_nothing_selected": {"hu": "Nincs kijelölve semmi.", "en": "Nothing selected."}, - "log_nothing_to_do": {"hu": "Nincs végrehajtható művelet.", "en": "No action to perform."}, - "log_action_start": {"hu": "{action} indul ({n} app)…", "en": "{action} starting ({n} app)…"}, - "log_done": {"hu": "\nKész: {ok}/{total} lépés sikeres.", "en": "\nDone: {ok}/{total} steps succeeded."}, - "bk_start_backup": {"hu": "Mentés indul -> {dest}", "en": "Backup starting -> {dest}"}, - "bk_start_restore": {"hu": "Visszaállítás indul <- {dest}", "en": "Restore starting <- {dest}"}, - "bk_need_dest": {"hu": "Add meg a célfájlt.", "en": "Specify the destination file."}, - "bk_invalid_file": {"hu": "Nem létező vagy érvénytelen backup fájl.", "en": "Backup file doesn't exist or is invalid."}, - "bk_need_selection": {"hu": "Jelölj ki legalább egy elemet.", "en": "Select at least one item."}, - "bk_done_backup_ok": {"hu": "Mentés kész.", "en": "Backup complete."}, - "bk_done_restore_ok": {"hu": "Visszaállítás kész.", "en": "Restore complete."}, - "bk_result_ok": {"hu": "OK", "en": "OK"}, - "bk_result_err": {"hu": "HIBA", "en": "ERROR"}, - "filedialog_caption": {"hu": "Backup fájl", "en": "Backup file"}, - "not_installed_hint": {"hu": " [kihagyva, nem létezik]", "en": " [skipped, doesn't exist]"}, - "lang_hu": {"hu": "Magyar", "en": "Magyar"}, - "lang_en": {"hu": "English", "en": "English"}, -} - - -def tr(key, **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 - - -def _loc(val): - """Katalógus-adat (app-list.json / backup-paths.json) i18n feloldása. - Ha a mező {"hu":..,"en":..} alakú, az aktuális nyelvre bontja, kulcs - hiányában hu-ra esik vissza; sima stringnél visszaadja változatlanul.""" - if isinstance(val, dict): - return val.get(_current_lang, val.get("hu", "")) - return val or "" - - -def build_qss() -> str: - return f""" - QWidget {{ - background: {C['bg']}; - color: {C['text']}; - font-size: 13px; - }} - QToolTip {{ - background: {C['surface']}; - color: {C['text']}; - border: 1px solid {C['border']}; - padding: 4px; - }} - - /* Fejléc sáv */ - QFrame#Header {{ - background: {C['header']}; - border: none; - border-bottom: 1px solid {C['border']}; - }} - - /* Kártya-szerű felületek */ - QFrame#Card, QScrollArea, QFrame#ListHost {{ - background: {C['bg']}; - border: none; - }} - - /* Label-ek átlátszó háttere (ne nyomja rá a QWidget bg-t) */ - QLabel {{ background: transparent; }} - - /* App sorok */ - QFrame#Row {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - border-radius: 4px; - }} - QFrame#Row:hover {{ - background: {C['surface_hi']}; - border: 1px solid {C['border_hi']}; - }} - - /* Kereső / combo */ - QLineEdit, QComboBox {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - border-radius: 4px; - padding: 7px 10px; - color: {C['text']}; - selection-background-color: {C['accent_dim']}; - }} - QLineEdit:focus, QComboBox:focus {{ - border: 1px solid {C['accent']}; - }} - QComboBox::drop-down {{ border: none; width: 22px; }} - QComboBox QAbstractItemView {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - selection-background-color: {C['accent_dim']}; - outline: none; - }} - - /* Szegmentált fülek (All / Installed) */ - QPushButton#Tab {{ - background: transparent; - border: 1px solid {C['border']}; - border-radius: 4px; - padding: 6px 18px; - color: {C['text_dim']}; - }} - QPushButton#Tab:hover {{ color: {C['text']}; border-color: {C['border_hi']}; }} - QPushButton#Tab:checked {{ - background: {C['surface_hi']}; - color: {C['text']}; - border-color: {C['border_hi']}; - }} - - /* Gombok */ - QPushButton {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - border-radius: 4px; - padding: 9px 18px; - color: {C['text']}; - }} - QPushButton:hover {{ background: {C['surface_hi']}; border-color: {C['border_hi']}; }} - QPushButton:disabled {{ color: {C['text_mut']}; border-color: {C['border']}; }} - - QPushButton#Primary {{ - background: {C['accent']}; - border: 1px solid {C['accent']}; - color: #15240f; - font-weight: 600; - }} - QPushButton#Primary:hover {{ background: {C['accent_hi']}; border-color: {C['accent_hi']}; }} - QPushButton#Primary:disabled {{ background: {C['accent_dim']}; border-color: {C['accent_dim']}; color: #2a3a22; }} - - QPushButton#Danger {{ border-color: {C['danger']}; color: {C['danger_hi']}; }} - QPushButton#Danger:hover {{ background: {C['danger']}; color: {C['text']}; }} - QPushButton#Danger:disabled {{ color: {C['text_mut']}; border-color: {C['border']}; }} - - /* Checkbox: zöld kitöltés bekapcsoláskor (asset nélkül) */ - QCheckBox {{ spacing: 0; }} - QCheckBox::indicator {{ - width: 17px; height: 17px; - border: 1px solid {C['border_hi']}; - border-radius: 3px; - background: transparent; - }} - QCheckBox::indicator:hover {{ border-color: {C['accent']}; }} - QCheckBox::indicator:checked {{ - background: {C['accent']}; - border: 1px solid {C['accent']}; - }} - - /* ArtCraft legördülő (több választós) */ - QToolButton#Choice {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - border-radius: 4px; - padding: 4px 10px; - color: {C['text']}; - font-size: 12px; - }} - QToolButton#Choice:hover {{ - background: {C['surface_hi']}; - border-color: {C['border_hi']}; - }} - QToolButton#Choice::menu-indicator {{ image: none; width: 0; }} - QMenu {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - padding: 4px; - }} - QMenu::item {{ - padding: 5px 22px 5px 26px; - border-radius: 3px; - color: {C['text']}; - }} - QMenu::item:selected {{ background: {C['surface_hi']}; }} - QMenu::item:disabled {{ color: {C['text_mut']}; }} - QMenu::indicator {{ width: 14px; height: 14px; left: 8px; }} - QMenu::indicator:unchecked {{ - border: 1px solid {C['border_hi']}; - border-radius: 3px; - background: transparent; - }} - QMenu::indicator:checked {{ - background: {C['accent']}; - border: 1px solid {C['accent']}; - border-radius: 3px; - }} - - /* Log */ - QPlainTextEdit#Log {{ - background: {C['header']}; - border: 1px solid {C['border']}; - border-radius: 4px; - color: {C['text_dim']}; - font-family: "JetBrains Mono", "DejaVu Sans Mono", monospace; - font-size: 12px; - padding: 6px; - }} - - /* Progress */ - QProgressBar {{ - background: {C['surface']}; - border: 1px solid {C['border']}; - border-radius: 4px; - text-align: center; - color: {C['text']}; - height: 22px; - }} - QProgressBar::chunk {{ background: {C['accent']}; border-radius: 3px; }} - - /* Scrollbar */ - QScrollBar:vertical {{ background: transparent; width: 10px; margin: 0; }} - QScrollBar::handle:vertical {{ - background: {C['border_hi']}; border-radius: 5px; min-height: 28px; - }} - QScrollBar::handle:vertical:hover {{ background: {C['text_mut']}; }} - QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; }} - QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }} - """ - - - -def load_catalog() -> list: - """app-list.json beolvasása az első létező útvonalról.""" - for path in APP_LIST_CANDIDATES: - if path.is_file(): - try: - data = json.loads(path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError) as exc: - raise RuntimeError(f"Hibás app-list.json ({path}): {exc}") - apps = data.get("apps", data) if isinstance(data, dict) else data - cleaned = [] - for a in apps: - if not isinstance(a, dict) or "name" not in a or "id" not in a: - continue - a.setdefault("type", "pacman") - a.setdefault("category", "Other") - cleaned.append(a) - cleaned.sort(key=lambda x: x["name"].lower()) - return cleaned - raise RuntimeError( - "Nem található app-list.json. Keresett helyek:\n " - + "\n ".join(str(p) for p in APP_LIST_CANDIDATES) - ) - - -def _query_set(cmd) -> set: - try: - out = subprocess.run(cmd, capture_output=True, text=True, timeout=20) - return {l.strip() for l in out.stdout.splitlines() if l.strip()} - except (subprocess.SubprocessError, FileNotFoundError, OSError): - return set() - - -def installed_index() -> dict: - """Egyetlen lekérdezés backendenként -> gyors membership check.""" - pac = _query_set(["pacman", "-Qq"]) - flat = _query_set(["flatpak", "list", "--app", "--columns=application"]) - return {"pacman": pac, "flatpak": flat} - - -def is_installed(app: dict, idx: dict) -> bool: - t = app.get("type", "pacman") - - if t in ("pacman", "aur"): - packages = app.get("packages") or [app["id"]] - return all(p in idx["pacman"] for p in packages) - - if t == "flatpak-user": - fid = app.get("flatpak_id", "") - return bool(fid) and fid in idx["flatpak"] - - if t == "script": - check_pkgs = app.get("check_packages") - if check_pkgs: - return all(p in idx["pacman"] for p in check_pkgs) - chk = app.get("check") - if not chk: - return False - if chk.startswith("/") and Path(chk).exists(): - return True - try: - return subprocess.run(["bash", "-c", chk], - capture_output=True, timeout=15).returncode == 0 - except (subprocess.SubprocessError, OSError): - return False - - if t == "choice": - return any(_option_installed(o, idx) for o in app.get("options", [])) - - return False - - - -def _option_installed(opt: dict, idx: dict) -> bool: - pkgs = opt.get("packages") or [opt["id"]] - return all(p in idx.get("pacman", set()) for p in pkgs) - - -def _all_options_installed(app: dict, idx: dict) -> bool: - opts = app.get("options", []) - return bool(opts) and all(_option_installed(o, idx) for o in opts) - - -def _root_prefix(gui: bool) -> list: - """GUI -> sudo -S (jelszó stdin-ről), terminál -> sudo, ha már root -> semmi.""" - if os.geteuid() == 0: - return [] - return ["sudo", "-S"] - - -def plan_commands(apps: list, install: bool, gui: bool) -> list: - """ - Visszaad egy listát: [(címke, parancs_lista), ...]. - Backendenként batch-eli a csomagokat -> kevesebb pkexec prompt. - """ - import shlex - pac, aur, flat, scripts, post = [], [], [], [], [] - - for a in apps: - t = a.get("type", "pacman") - if t == "pacman": - pkgs = a.get("packages") or [a["id"]] - pac.extend(pkgs) - if install: - for cmd_str in a.get("post_install", []): - post.append((f"post: {a['name']}", shlex.split(cmd_str))) - elif t == "aur": - pkgs = a.get("packages") or [a["id"]] - aur.extend(pkgs) - elif t == "flatpak-user": - fid = a.get("flatpak_id") - if fid: - flat.append((fid, a.get("post_install", []), a["name"])) - elif t == "script": - if install or a.get("removable", True): - scripts.append(a) - - rp = _root_prefix(gui) - steps = [] - - if pac: - if install: - steps.append((f"pacman: {', '.join(pac)}", - rp + ["pacman", "-S", "--needed", "--noconfirm", *pac])) - else: - steps.append((f"pacman -R: {', '.join(pac)}", - rp + ["pacman", "-Rns", "--noconfirm", *pac])) - - if aur: - if install: - steps.append((f"AUR: {', '.join(aur)}", - ["yay", "-S", "--needed", "--noconfirm", - "--answerclean", "None", "--answerdiff", "None", - "--answeredit", "None", "--answerupgrade", "None", - *aur])) - else: - steps.append((f"AUR -R: {', '.join(aur)}", - ["yay", "-Rns", "--noconfirm", *aur])) - - if flat and install: - steps.append(("flathub remote", [ - "flatpak", "remote-add", "--if-not-exists", "--user", - "flathub", "https://dl.flathub.org/repo/flathub.flatpakrepo" - ])) - - for fid, post_cmds, name in flat: - if install: - steps.append((f"flatpak: {fid}", - ["flatpak", "install", "--user", "--noninteractive", - "--or-update", "flathub", fid])) - for cmd_str in post_cmds: - steps.append((f"post: {name}", shlex.split(cmd_str))) - else: - steps.append((f"flatpak uninstall: {fid}", - ["flatpak", "uninstall", "--user", "-y", fid])) - - if install: - _home = os.path.expanduser("~") - for a in apps: - if a.get("type") == "script": - continue - cdir = a.get("config_dir") - cdest = a.get("config_dest") - if not cdir or not cdest: - continue - src = _SCRIPT_DIR / "data" / "configs" / cdir - if not src.is_dir(): - continue - dest = cdest.replace("{HOME}", _home) - cp_flag = "-rf" if a.get("config_overwrite") else "-rn" - steps.append((f"config: {a['name']}", - ["bash", "-c", - f"mkdir -p {shlex.quote(dest)} && " - f"cp {cp_flag} {shlex.quote(str(src))}/. {shlex.quote(dest)}/ && " - f"rm -f {shlex.quote(dest)}/Singleton*"])) - - if install: - _home = os.path.expanduser("~") - for a in apps: - if a.get("type") == "script": - continue - for mapping in a.get("config_mappings", []): - msrc = mapping.get("source") - mdest = mapping.get("dest") - if not msrc or not mdest: - continue - src = _SCRIPT_DIR / "data" / "configs" / msrc - if not src.is_dir(): - continue - dest = mdest.replace("{HOME}", _home) - cp_flag = "-rf" if a.get("config_overwrite") else "-rn" - steps.append((f"config: {a['name']} ({msrc})", - ["bash", "-c", - f"mkdir -p {shlex.quote(dest)} && " - f"cp {cp_flag} {shlex.quote(str(src))}/. {shlex.quote(dest)}/ && " - f"rm -f {shlex.quote(dest)}/Singleton*"])) - - _caller = os.environ.get("USER") or os.environ.get("LOGNAME") or "" - _caller_home = os.path.expanduser("~") - _caller_uid = str(os.getuid()) - _xdg_runtime = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{_caller_uid}") - _config_dir = str(_SCRIPT_DIR / "data" / "configs") - _script_env = [ - "env", - f"INSTALL_USER={_caller}", - f"INSTALL_HOME={_caller_home}", - f"TARGET_UID={_caller_uid}", - f"TARGET_XDG_RUNTIME_DIR={_xdg_runtime}", - f"RAVEOS_CONFIG_PACKAGE_DIR={_config_dir}", - ] - - for a in scripts: - script_name = a.get("script", "") - script_path = _SCRIPT_DIR / "data" / "actions" / script_name - if script_name and script_path.exists(): - action = "install" if install else "remove" - steps.append((f"script: {a['name']}", - rp + _script_env + ["bash", str(script_path), action])) - - steps.extend(post) - return steps - - -def stream_command(label: str, cmd: list, emit, password: str = "") -> bool: - """Parancs futtatása élő kimenettel. emit(str) -> log sor. True ha sikeres.""" - emit(f"$ {' '.join(cmd)}") - try: - needs_pw = len(cmd) >= 2 and cmd[0] == "sudo" and cmd[1] == "-S" - stdin_pipe = subprocess.PIPE if needs_pw else None - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, stdin=stdin_pipe, - text=True, bufsize=1) - if needs_pw and password: - proc.stdin.write(password + "\n") - proc.stdin.flush() - proc.stdin.close() - except (FileNotFoundError, OSError) as exc: - emit(f"[HIBA] nem indítható: {exc}") - return False - for line in proc.stdout: - emit(line.rstrip("\n")) - proc.wait() - ok = proc.returncode == 0 - emit(f"[{'OK' if ok else 'HIBA(' + str(proc.returncode) + ')'}] {label}") - return ok - - - -BACKUP_LIST_CANDIDATES = [ - _SCRIPT_DIR / "data/backup/backup-paths.json", - Path("/usr/share/raveos/backup-paths.json"), -] - - -def load_backup_catalog() -> list: - for path in BACKUP_LIST_CANDIDATES: - if path.is_file(): - try: - data = json.loads(path.read_text(encoding="utf-8")) - return data.get("groups", []) - except (json.JSONDecodeError, OSError): - return [] - return [] - - -def run_gui() -> int: - from PyQt6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QPushButton, QLineEdit, QComboBox, QScrollArea, QFrame, - QCheckBox, QPlainTextEdit, QProgressBar, QButtonGroup, QSizePolicy, - QMessageBox, QFileDialog, QDialog, QToolButton, QMenu, - ) - from PyQt6.QtCore import Qt, QThread, pyqtSignal - from PyQt6.QtGui import QFont, QFontDatabase, QAction - - def display_font(size: int, weight=QFont.Weight.Normal) -> QFont: - for fam in ("Bebas Neue", "Oswald", "Anton"): - if fam in QFontDatabase.families(): - f = QFont(fam, size) - f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 104) - return f - f = QFont() - f.setPointSize(size) - f.setBold(True) - f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 102) - return f - - class Worker(QThread): - log = pyqtSignal(str) - progress = pyqtSignal(int) - done = pyqtSignal(int, int) - - def __init__(self, steps, password: str = ""): - super().__init__() - self.steps = steps - self.password = password - - def run(self): - total = len(self.steps) - ok = 0 - for i, (label, cmd) in enumerate(self.steps): - self.log.emit("") - self.log.emit(f"── {label} ──") - if stream_command(label, cmd, self.log.emit, self.password): - ok += 1 - self.progress.emit(int((i + 1) / total * 100)) - self.done.emit(ok, total) - - class CheckableMenu(QMenu): - def mouseReleaseEvent(self, e): - act = self.activeAction() - if act is not None and act.isEnabled() and act.isCheckable(): - act.setChecked(not act.isChecked()) - e.accept() - return - super().mouseReleaseEvent(e) - - def keyPressEvent(self, e): - if e.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space): - act = self.activeAction() - if act is not None and act.isEnabled() and act.isCheckable(): - act.setChecked(not act.isChecked()) - e.accept() - return - super().keyPressEvent(e) - - class Row(QFrame): - def __init__(self, app, installed, idx=None, only_inst=False): - super().__init__() - self.app = app - self.installed = installed - self.idx = idx or {"pacman": set(), "flatpak": set()} - self.is_message = app.get("type") == "message" - self.is_choice = app.get("type") == "choice" - self.option_actions = [] - self.setObjectName("Row") - lay = QHBoxLayout(self) - lay.setContentsMargins(12, 9, 12, 9) - lay.setSpacing(10) - - self.cb = QCheckBox() - if self.is_message: - self.cb.setVisible(False) - self.cb.setEnabled(False) - lay.addWidget(self.cb) - - mid = QVBoxLayout() - mid.setSpacing(2) - top = QLabel(app["name"]) - top.setStyleSheet(f"font-weight:600; font-size:14px; color:{C['text']};") - desc = _loc(app.get("desc")) - sub = QLabel(f"{app['category']} · {TYPE_LABEL.get(app['type'], app['type'])}" - + (f" · {desc}" if desc else "")) - sub.setStyleSheet(f"color:{C['text_dim']}; font-size:12px;") - mid.addWidget(top) - mid.addWidget(sub) - lay.addLayout(mid, 1) - - if self.is_choice: - self.menu_btn = QToolButton() - self.menu_btn.setObjectName("Choice") - self.menu_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) - menu = CheckableMenu(self.menu_btn) - for o in app.get("options", []): - oinst = _option_installed(o, self.idx) - if only_inst and not oinst: - continue - act = QAction(o["name"] + (" ✓" if oinst else ""), menu) - act.setCheckable(True) - odesc = _loc(o.get("desc")) - if odesc: - act.setToolTip(odesc) - act.toggled.connect(self._choice_sync) - menu.addAction(act) - self.option_actions.append((act, o)) - if not self.option_actions: - act = QAction("-", menu) - act.setEnabled(False) - menu.addAction(act) - self.menu_btn.setMenu(menu) - self.menu_btn.setFixedWidth(180) - lay.addWidget(self.menu_btn) - self.cb.toggled.connect(self._choice_toggle_all) - self._choice_sync() - - if self.is_message: - status = QLabel(tr("row_info")) - status.setStyleSheet(f"color:{C['accent_hi']}; font-size:12px;") - elif self.is_choice: - total = len(app.get("options", [])) - names = [o["name"] for o in app.get("options", []) - if _option_installed(o, self.idx)] - n = len(names) - status = QLabel( - tr("choice_installed", n=n, total=total) if n else tr("row_not_installed")) - status.setStyleSheet( - f"color:{C['accent_hi'] if n else C['text_mut']}; font-size:12px;") - if names: - status.setToolTip(", ".join(names)) - else: - status = QLabel(tr("row_installed") if installed else tr("row_not_installed")) - status.setStyleSheet( - f"color:{C['accent_hi'] if installed else C['text_mut']}; font-size:12px;") - lay.addWidget(status) - - self.setCursor(Qt.CursorShape.PointingHandCursor) - - def _choice_sync(self): - names = [o["name"] for act, o in self.option_actions if act.isChecked()] - n = len(names) - if n == 0: - text = tr("choice_pick") - elif n == 1: - text = names[0] - else: - text = tr("choice_selected", n=n) - self.menu_btn.setText(text + " ▾") - self.menu_btn.setToolTip(", ".join(names) if names else tr("choice_pick")) - self.cb.blockSignals(True) - self.cb.setChecked(n > 0) - self.cb.blockSignals(False) - - def _choice_toggle_all(self, checked): - for act, o in self.option_actions: - act.blockSignals(True) - act.setChecked(checked) - act.blockSignals(False) - self._choice_sync() - - def mousePressEvent(self, e): - if e.button() == Qt.MouseButton.LeftButton: - if self.is_message: - QMessageBox.information( - self, self.app["name"], self.app.get("message", "")) - else: - self.cb.toggle() - super().mousePressEvent(e) - - class SbWorker(QThread): - log = pyqtSignal(str) - done = pyqtSignal(int) - - def __init__(self, mode, password): - super().__init__() - self.mode = mode - self.password = password - - def run(self): - cmd = ["sudo", "-S", "-p", "", "raveos-secureboot-setup", self.mode] - self.log.emit(f"$ {' '.join(cmd)}") - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - stdin=subprocess.PIPE, text=True, bufsize=1) - if self.password: - proc.stdin.write(self.password + "\n") - proc.stdin.flush() - proc.stdin.close() - except OSError as exc: - self.log.emit(f"[HIBA] {exc}") - self.done.emit(1) - return - for line in proc.stdout: - self.log.emit(line.rstrip("\n")) - proc.wait() - self.done.emit(proc.returncode) - - class BackupWorker(QThread): - log = pyqtSignal(str) - progress = pyqtSignal(int) - done = pyqtSignal(bool, str) - - def __init__(self, paths, dest, restore=False, password=""): - super().__init__() - self.paths = paths - self.dest = dest - self.restore = restore - self.password = password - - def _sudo_run(self, cmd, timeout=60): - if os.geteuid() == 0: - return subprocess.run(cmd, capture_output=True, timeout=timeout) - return subprocess.run( - ["sudo", "-S", *cmd], input=(self.password + "\n").encode(), - capture_output=True, timeout=timeout, - ) - - def _backup_root_item(self, tf, p): - """Root-vedett fajl/konyvtar hozzaadasa sudo tar streammel.""" - import io - import tarfile - parent = str(Path(p).parent) - name = Path(p).name - proc = self._sudo_run(["tar", "-cf", "-", "-C", parent, name], timeout=120) - if proc.returncode != 0: - raise RuntimeError(proc.stderr.decode(errors="replace").strip()[:200] or "sudo tar sikertelen") - with tarfile.open(fileobj=io.BytesIO(proc.stdout)) as inner: - for member in inner.getmembers(): - f = inner.extractfile(member) if member.isfile() else None - tf.addfile(member, f) - - def _restore_root_item(self, tf, member, dest_path): - """Egy tar tagot kozvetlenul sudo tar extract-tel allit vissza, hogy - az eredeti tulajdonos/jog megmaradjon - egy nem-privilegizalt - kibontas (pl. sudo cp egy user-tulajdonu temp fajlbol) elveszitene - az eredeti (pl. root:root) tulajdonost. - - A jelszot SUDO_ASKPASS-szal adjuk at, NEM a "sudo -S" stdin - trukkel, mert itt a tar extract sajat binaris payload-ja is a - stdin-en jon - ha a jelszo ugyanoda irodna, sudo tobbet olvasna - ki a bufferelt read()-jevel mint a jelszo sora, es osszekeveredne - a tar adattal ("not a tar archive" hiba). - """ - import io - import shlex - import tarfile - import tempfile - - fileobj = tf.extractfile(member) if member.isfile() else None - buf = io.BytesIO() - with tarfile.open(fileobj=buf, mode="w") as mini: - mini.addfile(member, fileobj) - - dest_parent = str(Path(dest_path).parent) - self._sudo_run(["mkdir", "-p", dest_parent], timeout=15) - - if os.geteuid() == 0: - proc = subprocess.run( - ["tar", "-xf", "-", "-C", dest_parent], - input=buf.getvalue(), capture_output=True, timeout=60, - ) - else: - fd, askpass = tempfile.mkstemp(prefix="raveos-askpass-", suffix=".sh") - try: - with os.fdopen(fd, "w") as f: - f.write(f"#!/bin/sh\nprintf '%s\\n' {shlex.quote(self.password)}\n") - os.chmod(askpass, 0o700) - env = os.environ.copy() - env["SUDO_ASKPASS"] = askpass - proc = subprocess.run( - ["sudo", "-A", "tar", "-xf", "-", "-C", dest_parent], - input=buf.getvalue(), capture_output=True, timeout=60, env=env, - ) - finally: - os.unlink(askpass) - - if proc.returncode != 0: - raise RuntimeError( - proc.stderr.decode(errors="replace").strip()[:200] or "sudo tar extract sikertelen") - - def run(self): - import tarfile - total = len(self.paths) - if not self.restore: - try: - with tarfile.open(self.dest, "w:gz") as tf: - for i, (label, p, requires_root) in enumerate(self.paths): - self.log.emit(f"+ {label} ({p})") - try: - if requires_root: - self._backup_root_item(tf, p) - elif Path(p).exists(): - tf.add(p, arcname=Path(p).name) - else: - self.log.emit(tr("not_installed_hint")) - except Exception as exc: - self.log.emit(f" [HIBA, kihagyva: {exc}]") - self.progress.emit(int((i + 1) / total * 100)) - self.done.emit(True, tr("bk_done_backup_ok")) - except Exception as exc: - self.done.emit(False, str(exc)) - else: - try: - root_dest = { - Path(p).name: p for _, p, requires_root in self.paths if requires_root - } - home_base = str(Path(self.paths[0][1]).parent) - with tarfile.open(self.dest, "r:gz") as tf: - total_members = len(tf.getmembers()) - for i, member in enumerate(tf.getmembers()): - self.log.emit(f"< {member.name}") - top = member.name.split("/")[0] - try: - if top in root_dest: - self._restore_root_item(tf, member, root_dest[top]) - else: - tf.extract(member, path=home_base, filter="data") - except Exception as exc: - self.log.emit(f" [HIBA, kihagyva: {exc}]") - self.progress.emit(int((i + 1) / total_members * 100)) - self.done.emit(True, tr("bk_done_restore_ok")) - except Exception as exc: - self.done.emit(False, str(exc)) - - class Main(QMainWindow): - def __init__(self, catalog): - super().__init__() - from PyQt6.QtWidgets import QStackedWidget - self.catalog = catalog - self.idx = installed_index() - self.rows = [] - self.worker = None - self.sb_worker = None - self.sb_data = None - self.backup_worker = None - self.backup_checkboxes = {} - self._sudo_pw = "" - self._sudo_pw_ts = 0.0 - - self.setWindowTitle(APP_NAME) - self.resize(1060, 820) - - root = QWidget() - self.setCentralWidget(root) - outer = QVBoxLayout(root) - outer.setContentsMargins(0, 0, 0, 0) - outer.setSpacing(0) - - header = QFrame() - header.setObjectName("Header") - hl = QHBoxLayout(header) - hl.setContentsMargins(20, 14, 20, 14) - wm = QHBoxLayout() - wm.setSpacing(0) - rave = QLabel("RAVE") - rave.setFont(display_font(26)) - rave.setStyleSheet(f"color:{C['text']};") - os_ = QLabel("OS") - os_.setFont(display_font(26)) - os_.setStyleSheet(f"color:{C['accent']};") - wm.addWidget(rave) - wm.addWidget(os_) - self.header_sep = QLabel(tr("header_installer")) - self.header_sep.setFont(display_font(15)) - self.header_sep.setStyleSheet(f"color:{C['text_dim']};") - wm.addWidget(self.header_sep) - hl.addLayout(wm) - hl.addStretch(1) - - self.lang_hu_btn = QPushButton(tr("lang_hu")) - self.lang_hu_btn.setCheckable(True) - self.lang_hu_btn.setChecked(True) - self.lang_hu_btn.setFixedHeight(28) - self.lang_hu_btn.clicked.connect(lambda: self._set_lang("hu")) - hl.addWidget(self.lang_hu_btn) - self.lang_en_btn = QPushButton(tr("lang_en")) - self.lang_en_btn.setCheckable(True) - self.lang_en_btn.setChecked(False) - self.lang_en_btn.setFixedHeight(28) - self.lang_en_btn.clicked.connect(lambda: self._set_lang("en")) - hl.addWidget(self.lang_en_btn) - self._style_lang_btns() - hl.addSpacing(12) - - self.btn_sb = QPushButton(tr("nav_sb")) - self.btn_sb.clicked.connect(self._go_sb) - hl.addWidget(self.btn_sb) - hl.addSpacing(8) - - self.btn_nav = QPushButton(tr("nav_backup")) - self.btn_nav.clicked.connect(self._go_backup) - hl.addWidget(self.btn_nav) - hl.addSpacing(12) - ver = QLabel(f"v{APP_VERSION}") - ver.setStyleSheet(f"color:{C['text_mut']};") - hl.addWidget(ver) - outer.addWidget(header) - - self.stack = QStackedWidget() - outer.addWidget(self.stack, 1) - - apps_page = QWidget() - bl = QVBoxLayout(apps_page) - bl.setContentsMargins(20, 16, 20, 16) - bl.setSpacing(12) - - frow = QHBoxLayout() - frow.setSpacing(8) - self.tab_all = QPushButton(tr("tab_all")) - self.tab_inst = QPushButton(tr("tab_installed")) - for t in (self.tab_all, self.tab_inst): - t.setObjectName("Tab") - t.setCheckable(True) - self.tab_all.setChecked(True) - grp = QButtonGroup(self) - grp.setExclusive(True) - grp.addButton(self.tab_all) - grp.addButton(self.tab_inst) - self.tab_all.clicked.connect(self._refresh_and_filter) - self.tab_inst.clicked.connect(self._refresh_and_filter) - frow.addWidget(self.tab_all) - frow.addWidget(self.tab_inst) - frow.addSpacing(8) - self.search = QLineEdit() - self.search.setPlaceholderText(tr("search_placeholder")) - self.search.textChanged.connect(self.apply_filter) - frow.addWidget(self.search, 1) - self.cat = QComboBox() - self.cat.addItems([tr("tab_all")] + sorted({a["category"] for a in catalog})) - self.cat.currentIndexChanged.connect(self.apply_filter) - frow.addWidget(self.cat) - bl.addLayout(frow) - - self.scroll = QScrollArea() - self.scroll.setWidgetResizable(True) - self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - host = QFrame() - host.setObjectName("ListHost") - self.list_lay = QVBoxLayout(host) - self.list_lay.setContentsMargins(0, 0, 6, 0) - self.list_lay.setSpacing(7) - self.list_lay.addStretch(1) - self.scroll.setWidget(host) - bl.addWidget(self.scroll, 1) - - self.log = QPlainTextEdit() - self.log.setObjectName("Log") - self.log.setReadOnly(True) - self.log.setFixedHeight(120) - self.log.hide() - bl.addWidget(self.log) - - arow = QHBoxLayout() - arow.setSpacing(8) - self.btn_install = QPushButton(tr("btn_install")) - self.btn_install.setObjectName("Primary") - self.btn_install.clicked.connect(lambda: self.run_action(True)) - self.btn_remove = QPushButton(tr("btn_remove")) - self.btn_remove.setObjectName("Danger") - self.btn_remove.clicked.connect(lambda: self.run_action(False)) - self.btn_remove.hide() - self.bar = QProgressBar() - self.bar.setValue(0) - arow.addWidget(self.btn_install) - arow.addWidget(self.btn_remove) - arow.addWidget(self.bar, 1) - bl.addLayout(arow) - - self.stack.addWidget(apps_page) - - bk_page = QWidget() - bbl = QVBoxLayout(bk_page) - bbl.setContentsMargins(20, 16, 20, 16) - bbl.setSpacing(12) - - bk_scroll = QScrollArea() - bk_scroll.setWidgetResizable(True) - bk_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - bk_host = QWidget() - self.bk_vl = QVBoxLayout(bk_host) - self.bk_vl.setContentsMargins(0, 0, 6, 0) - self.bk_vl.setSpacing(14) - self.build_backup_items() - bk_scroll.setWidget(bk_host) - bbl.addWidget(bk_scroll, 1) - - dest_row = QHBoxLayout() - dest_row.setSpacing(8) - self.dest_edit = QLineEdit() - self.dest_edit.setPlaceholderText(tr("dest_placeholder")) - self.dest_edit.setText(str(Path.home() / "raveos-backup.tar.gz")) - self.btn_bk_browse = QPushButton(tr("btn_browse")) - self.btn_bk_browse.clicked.connect(self._bk_browse) - dest_row.addWidget(self.dest_edit, 1) - dest_row.addWidget(self.btn_bk_browse) - bbl.addLayout(dest_row) - - self.bk_log = QPlainTextEdit() - self.bk_log.setObjectName("Log") - self.bk_log.setReadOnly(True) - self.bk_log.setFixedHeight(120) - self.bk_log.hide() - bbl.addWidget(self.bk_log) - - bk_arow = QHBoxLayout() - bk_arow.setSpacing(8) - self.btn_bk_save = QPushButton(tr("btn_save")) - self.btn_bk_save.setObjectName("Primary") - self.btn_bk_save.clicked.connect(self._do_backup) - self.btn_bk_restore = QPushButton(tr("btn_restore")) - self.btn_bk_restore.setObjectName("Danger") - self.btn_bk_restore.clicked.connect(self._do_restore) - self.bk_bar = QProgressBar() - self.bk_bar.setValue(0) - bk_arow.addWidget(self.btn_bk_save) - bk_arow.addWidget(self.btn_bk_restore) - bk_arow.addWidget(self.bk_bar, 1) - bbl.addLayout(bk_arow) - - self.stack.addWidget(bk_page) - - sb_page = QWidget() - sbl = QVBoxLayout(sb_page) - sbl.setContentsMargins(20, 16, 20, 16) - sbl.setSpacing(12) - - self.sb_desc = QLabel(tr("sb_desc")) - self.sb_desc.setWordWrap(True) - self.sb_desc.setStyleSheet(f"color:{C['text_dim']};") - sbl.addWidget(self.sb_desc) - - self.sb_beta = QLabel(tr("sb_beta")) - self.sb_beta.setWordWrap(True) - self.sb_beta.setStyleSheet("color:#d0a24c; font-weight:600;") - sbl.addWidget(self.sb_beta) - - self.sb_lbls = {} - self.sb_vals = {} - srow1 = QHBoxLayout() - srow1.setSpacing(8) - srow2 = QHBoxLayout() - srow2.setSpacing(8) - for i, key in enumerate(("sb_lbl_sb", "sb_lbl_setup", "sb_lbl_keys", "sb_lbl_signed")): - lbl = QLabel(tr(key)) - lbl.setStyleSheet(f"color:{C['text_dim']};") - val = QLabel("-") - val.setStyleSheet(f"color:{C['text']}; font-weight:600;") - self.sb_lbls[key] = lbl - self.sb_vals[key] = val - box = QHBoxLayout() - box.setSpacing(6) - box.addWidget(lbl) - box.addWidget(val) - (srow1 if i < 2 else srow2).addLayout(box) - (srow1 if i < 2 else srow2).addSpacing(24) - srow1.addStretch(1) - srow2.addStretch(1) - sbl.addLayout(srow1) - sbl.addLayout(srow2) - - self.sb_warn = QLabel(tr("sb_bl_warn")) - self.sb_warn.setWordWrap(True) - self.sb_warn.setStyleSheet("color:#d0a24c;") - sbl.addWidget(self.sb_warn) - - self.sb_pending = QLabel(tr("sb_pending")) - self.sb_pending.setStyleSheet("color:#d0a24c; font-weight:700;") - self.sb_pending.hide() - sbl.addWidget(self.sb_pending) - - sb_row = QHBoxLayout() - sb_row.setSpacing(8) - self.btn_sb_refresh = QPushButton(tr("sb_btn_refresh")) - self.btn_sb_refresh.clicked.connect(self._sb_refresh) - self.btn_sb_setup = QPushButton(tr("sb_btn_setup")) - self.btn_sb_setup.setObjectName("Primary") - self.btn_sb_setup.clicked.connect(self._sb_setup) - self.btn_sb_resign = QPushButton(tr("sb_btn_resign")) - self.btn_sb_resign.clicked.connect(self._sb_resign) - sb_row.addWidget(self.btn_sb_refresh) - sb_row.addWidget(self.btn_sb_setup) - sb_row.addWidget(self.btn_sb_resign) - sb_row.addStretch(1) - sbl.addLayout(sb_row) - - self.sb_log = QPlainTextEdit() - self.sb_log.setObjectName("Log") - self.sb_log.setReadOnly(True) - sbl.addWidget(self.sb_log, 1) - - self.stack.addWidget(sb_page) - - self.build_rows() - self.apply_filter() - - def _style_lang_btns(self): - active = ( - f"QPushButton {{ background: {C['accent']}; color: #15240f;" - " border: 1px solid transparent; border-radius: 4px;" - " padding: 4px 12px; font-size: 11px; font-weight: 600; }}" - ) - inactive = ( - f"QPushButton {{ background: {C['surface']}; color: {C['text_dim']};" - f" border: 1px solid {C['border']}; border-radius: 4px;" - " padding: 4px 12px; font-size: 11px; font-weight: 600; }}" - ) - self.lang_hu_btn.setStyleSheet(active if _current_lang == "hu" else inactive) - self.lang_en_btn.setStyleSheet(active if _current_lang == "en" else inactive) - - 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") - self._style_lang_btns() - self._retranslate_all() - - def _retranslate_all(self): - idx = self.stack.currentIndex() - if idx == 1: - self.header_sep.setText(tr("header_backup")) - elif idx == 2: - self.header_sep.setText(tr("header_sb")) - else: - self.header_sep.setText(tr("header_installer")) - self.btn_nav.setText(tr("nav_back") if idx != 0 else tr("nav_backup")) - self.btn_sb.setText(tr("nav_sb")) - self.sb_desc.setText(tr("sb_desc")) - self.sb_beta.setText(tr("sb_beta")) - for key, lbl in self.sb_lbls.items(): - lbl.setText(tr(key)) - self.sb_warn.setText(tr("sb_bl_warn")) - self.sb_pending.setText(tr("sb_pending")) - self.btn_sb_refresh.setText(tr("sb_btn_refresh")) - self.btn_sb_setup.setText(tr("sb_btn_setup")) - self.btn_sb_resign.setText(tr("sb_btn_resign")) - self._sb_apply_values() - self.tab_all.setText(tr("tab_all")) - self.tab_inst.setText(tr("tab_installed")) - self.search.setPlaceholderText(tr("search_placeholder")) - cur_cat = self.cat.currentText() if self.cat.currentIndex() != 0 else None - self.cat.blockSignals(True) - self.cat.clear() - self.cat.addItems([tr("tab_all")] + sorted({a["category"] for a in self.catalog})) - if cur_cat is not None: - idx = self.cat.findText(cur_cat) - self.cat.setCurrentIndex(idx if idx >= 0 else 0) - self.cat.blockSignals(False) - self.btn_install.setText(tr("btn_install")) - self.btn_remove.setText(tr("btn_remove")) - self.dest_edit.setPlaceholderText(tr("dest_placeholder")) - self.btn_bk_browse.setText(tr("btn_browse")) - self.btn_bk_save.setText(tr("btn_save")) - self.btn_bk_restore.setText(tr("btn_restore")) - keep_scroll = self.scroll.verticalScrollBar().value() - self.build_rows() - self.apply_filter() - self.scroll.verticalScrollBar().setValue(keep_scroll) - self.build_backup_items() - - def build_backup_items(self): - prev_checked = {iid: cb.isChecked() for iid, (cb, _, _, _) in self.backup_checkboxes.items()} - while self.bk_vl.count(): - taken = self.bk_vl.takeAt(0) - w = taken.widget() - if w: - w.setParent(None) - self.backup_checkboxes = {} - home = str(Path.home()) - for bgrp in load_backup_catalog(): - gl = QLabel(_loc(bgrp.get("label", bgrp["id"]))) - gl.setStyleSheet(f"font-weight:600; font-size:13px; color:{C['accent']}; background:transparent;") - self.bk_vl.addWidget(gl) - desc = _loc(bgrp.get("description", "")) - if desc: - dl = QLabel(desc) - dl.setStyleSheet(f"color:{C['text_dim']}; font-size:11px; background:transparent;") - dl.setWordWrap(True) - self.bk_vl.addWidget(dl) - for item in bgrp.get("items", []): - brow = QFrame() - brow.setObjectName("Row") - rl = QHBoxLayout(brow) - rl.setContentsMargins(12, 7, 12, 7) - rl.setSpacing(10) - cb = QCheckBox() - cb.setChecked(prev_checked.get(item["id"], item.get("default_selected", False))) - abs_path = str(Path(home) / item["path"]) if item.get("scope","home") == "home" else item["path"] - label = _loc(item.get("label", item["id"])) - self.backup_checkboxes[item["id"]] = ( - cb, abs_path, label, - item.get("requires_root", False), - ) - rl.addWidget(cb) - mid = QVBoxLayout() - mid.setSpacing(1) - tl = QLabel(label) - tl.setStyleSheet(f"font-weight:600; font-size:13px; color:{C['text']}; background:transparent;") - sl = QLabel(abs_path) - sl.setStyleSheet(f"color:{C['text_dim']}; font-size:11px; background:transparent;") - mid.addWidget(tl) - mid.addWidget(sl) - note = _loc(item.get("note")) - if note: - nl = QLabel(note) - nl.setStyleSheet(f"color:{C['danger_hi']}; font-size:11px; background:transparent;") - mid.addWidget(nl) - rl.addLayout(mid, 1) - if item.get("size_hint"): - hl2 = QLabel(item["size_hint"]) - hl2.setStyleSheet(f"color:{C['text_mut']}; font-size:11px; background:transparent;") - rl.addWidget(hl2) - self.bk_vl.addWidget(brow) - self.bk_vl.addStretch(1) - - def _refresh_and_filter(self): - self.idx = installed_index() - self.build_rows() - self.apply_filter() - - def _go_backup(self): - self.stack.setCurrentIndex(1) - self.header_sep.setText(tr("header_backup")) - self.btn_nav.setText(tr("nav_back")) - self.btn_nav.clicked.disconnect() - self.btn_nav.clicked.connect(self._go_apps) - - def _go_apps(self): - self.stack.setCurrentIndex(0) - self.header_sep.setText(tr("header_installer")) - self.btn_nav.setText(tr("nav_backup")) - self.btn_nav.clicked.disconnect() - self.btn_nav.clicked.connect(self._go_backup) - - def _go_sb(self): - self.stack.setCurrentIndex(2) - self.header_sep.setText(tr("header_sb")) - self.btn_nav.setText(tr("nav_back")) - self.btn_nav.clicked.disconnect() - self.btn_nav.clicked.connect(self._go_apps) - self._sb_refresh() - - def _sb_apply_values(self): - data = self.sb_data - if not data: - return - on = lambda x: tr("sb_val_on") if x == "1" else tr("sb_val_off") - yes = lambda x: tr("sb_val_yes") if x == "1" else tr("sb_val_no") - ok = lambda x: tr("sb_val_ok") if x == "1" else tr("sb_val_missing") - self.sb_vals["sb_lbl_sb"].setText(on(data.get("secureboot", "?"))) - self.sb_vals["sb_lbl_setup"].setText(yes(data.get("setupmode", "?"))) - self.sb_vals["sb_lbl_keys"].setText(ok(data.get("keys", "0"))) - self.sb_vals["sb_lbl_signed"].setText(ok(data.get("signed", "0"))) - self.sb_pending.setVisible(data.get("pending") == "1") - - def _sb_refresh(self): - self.sb_log.clear() - import time - out = "" - cached = self._sudo_pw if (self._sudo_pw and (time.time() - self._sudo_pw_ts) < 300) else "" - try: - for cmd in (["raveos-secureboot-setup", "--status"], - ["sudo", "-n", "raveos-secureboot-setup", "--status"]): - res = subprocess.run(cmd, capture_output=True, text=True, timeout=15) - out = res.stdout - if out.strip(): - break - if not out.strip() and cached: - res = subprocess.run(["sudo", "-S", "-p", "", "raveos-secureboot-setup", "--status"], - input=cached + "\n", capture_output=True, text=True, timeout=15) - out = res.stdout - except (OSError, subprocess.TimeoutExpired) as exc: - self.sb_log.appendPlainText(f"[HIBA] {exc}") - return - data = {} - for line in out.splitlines(): - if "=" in line and not line.startswith("["): - k, _, val = line.partition("=") - data[k.strip()] = val.strip() - elif line.strip(): - self.sb_log.appendPlainText(line) - if not data: - if not out.strip(): - self.sb_log.appendPlainText("[raveos-sb] Az allapot nem olvashato.") - return - self.sb_data = data - self._sb_apply_values() - - def _sb_run(self, mode): - if self.sb_worker and self.sb_worker.isRunning(): - return - import time - if os.geteuid() != 0: - if self._sudo_pw and (time.time() - self._sudo_pw_ts) < 300: - password = self._sudo_pw - else: - password = self._ask_password() - if not password: - return - self._sudo_pw = password - self._sudo_pw_ts = time.time() - else: - password = "" - self.sb_log.clear() - self.btn_sb_setup.setEnabled(False) - self.btn_sb_resign.setEnabled(False) - self.btn_sb_refresh.setEnabled(False) - self.sb_worker = SbWorker(mode, password) - self.sb_worker.log.connect(self.sb_log.appendPlainText) - self.sb_worker.done.connect(self._sb_finished) - self.sb_worker.start() - - def _sb_setup(self): - self._sb_run("--setup") - - def _sb_resign(self): - self._sb_run("--resign") - - def _sb_finished(self, rc): - self.btn_sb_setup.setEnabled(True) - self.btn_sb_resign.setEnabled(True) - self.btn_sb_refresh.setEnabled(True) - self._sb_refresh() - if rc == 0: - QMessageBox.information(self, tr("sb_done_title"), tr("sb_done_msg")) - elif rc == 10: - box = QMessageBox(self) - box.setWindowTitle(tr("sb_reboot_title")) - box.setText(tr("sb_reboot_msg")) - reboot_button = box.addButton(tr("sb_btn_reboot"), QMessageBox.ButtonRole.AcceptRole) - box.addButton(tr("sb_btn_cancel"), QMessageBox.ButtonRole.RejectRole) - box.exec() - if box.clickedButton() == reboot_button: - subprocess.Popen(["systemctl", "reboot"]) - else: - QMessageBox.critical(self, tr("sb_err_title"), tr("sb_err_msg")) - - def build_rows(self): - for r in self.rows: - r.setParent(None) - self.rows.clear() - only_inst = self.tab_inst.isChecked() - for app in self.catalog: - inst = is_installed(app, self.idx) - row = Row(app, inst, self.idx, only_inst) - self.list_lay.insertWidget(self.list_lay.count() - 1, row) - self.rows.append(row) - - def apply_filter(self): - q = self.search.text().strip().lower() - cat_idx = self.cat.currentIndex() - cat = self.cat.currentText() - only_inst = self.tab_inst.isChecked() - self.btn_remove.setVisible(only_inst) - self.btn_install.setVisible(not only_inst) - for r in self.rows: - show = True - is_choice = r.app.get("type") == "choice" - if only_inst and not r.installed: - show = False - if not only_inst: - if is_choice: - if _all_options_installed(r.app, self.idx): - show = False - elif r.installed: - show = False - if cat_idx != 0 and r.app["category"] != cat: - show = False - if q: - hay = (r.app["name"] + " " + _loc(r.app.get("desc"))).lower() - if is_choice: - hay += " " + " ".join( - o["name"] for o in r.app.get("options", [])).lower() - if q not in hay: - show = False - r.setVisible(show) - - def selected(self): - res = [] - for r in self.rows: - if not r.isVisible() or r.is_message: - continue - if r.app.get("type") == "choice": - if not r.cb.isChecked(): - continue - for act, o in r.option_actions: - if act.isChecked(): - res.append({ - **o, - "type": "pacman", - "category": r.app["category"], - "name": f"{r.app['name']}: {o['name']}", - }) - elif r.cb.isChecked(): - res.append(r.app) - return res - - def _ask_password(self) -> str: - """Jelszó dialog - egyszer fut az akció elején. Üres string = megszakítva.""" - err_msg = "" - while True: - dlg = QDialog(self) - dlg.setWindowTitle(tr("dlg_auth_title")) - dlg.setFixedWidth(360) - lay = QVBoxLayout(dlg) - lay.setSpacing(10) - lay.setContentsMargins(20, 16, 20, 16) - - lbl = QLabel(err_msg if err_msg else tr("dlg_pw_label")) - lbl.setStyleSheet( - f"color: {'#e06c6c' if err_msg else C['text_dim']}; font-size: 13px;") - lbl.setWordWrap(True) - lay.addWidget(lbl) - - pw_edit = QLineEdit() - pw_edit.setEchoMode(QLineEdit.EchoMode.Password) - pw_edit.setPlaceholderText(tr("dlg_pw_placeholder")) - pw_edit.setStyleSheet( - f"background: {C['surface']}; color: {C['text']};" - "border: 1px solid #444; border-radius: 4px;" - "padding: 6px 10px; font-size: 13px;") - lay.addWidget(pw_edit) - - btn_row = QHBoxLayout() - btn_ok = QPushButton(tr("dlg_ok")) - btn_ok.setDefault(True) - btn_cancel = QPushButton(tr("dlg_cancel")) - btn_ok.setFixedHeight(32) - btn_cancel.setFixedHeight(32) - btn_ok.setStyleSheet( - f"background: {C['accent']}; color: {C['text']};" - "border-radius: 4px; font-size: 13px;") - btn_cancel.setStyleSheet( - f"background: {C['surface']}; color: {C['text_dim']};" - "border-radius: 4px; font-size: 13px;") - btn_ok.clicked.connect(dlg.accept) - btn_cancel.clicked.connect(dlg.reject) - pw_edit.returnPressed.connect(dlg.accept) - btn_row.addStretch() - btn_row.addWidget(btn_cancel) - btn_row.addWidget(btn_ok) - lay.addLayout(btn_row) - - if not dlg.exec(): - return "" - - pw = pw_edit.text() - if not pw: - err_msg = tr("dlg_pw_empty_err") - continue - - try: - chk = subprocess.run( - ["sudo", "-S", "true"], - input=pw + "\n", - capture_output=True, - text=True, - timeout=8, - ) - if chk.returncode == 0: - return pw - err_msg = tr("dlg_pw_wrong_err") - except Exception: - return pw - - def run_action(self, install: bool): - apps = self.selected() - if not apps: - self.log.appendPlainText(tr("log_nothing_selected")) - return - if self.worker and self.worker.isRunning(): - return - steps = plan_commands(apps, install, gui=True) - if not steps: - self.log.appendPlainText(tr("log_nothing_to_do")) - return - - import time - if os.geteuid() != 0: - if self._sudo_pw and (time.time() - self._sudo_pw_ts) < 300: - password = self._sudo_pw - else: - password = self._ask_password() - if not password: - return - self._sudo_pw = password - self._sudo_pw_ts = time.time() - else: - password = "" - - self.set_busy(True) - self.bar.setValue(0) - self.log.clear() - self.log.show() - self.log.appendPlainText(tr( - "log_action_start", - action=tr("btn_install") if install else tr("btn_remove"), - n=len(apps), - )) - self.worker = Worker(steps, password) - self.worker.log.connect(self.log.appendPlainText) - self.worker.progress.connect(self.bar.setValue) - self.worker.done.connect(self.on_done) - self.worker.start() - - def on_done(self, ok, total): - self.bar.setValue(100) - self.log.appendPlainText(tr("log_done", ok=ok, total=total)) - self.idx = installed_index() - keep_scroll = self.scroll.verticalScrollBar().value() - self.build_rows() - self.apply_filter() - self.scroll.verticalScrollBar().setValue(keep_scroll) - self.set_busy(False) - - def set_busy(self, busy: bool): - self.btn_install.setEnabled(not busy) - self.btn_remove.setEnabled(not busy) - self.search.setEnabled(not busy) - self.cat.setEnabled(not busy) - - def _bk_browse(self): - path, _ = QFileDialog.getSaveFileName( - self, tr("filedialog_caption"), str(Path.home()), "Tar archive (*.tar.gz)") - if path: - if not path.endswith(".tar.gz"): - path += ".tar.gz" - self.dest_edit.setText(path) - - def _bk_selected(self): - return [(label, abs_path, requires_root) - for cb, abs_path, label, requires_root in self.backup_checkboxes.values() - if cb.isChecked()] - - def _bk_password_if_needed(self, paths): - """Jelszo bekerese, ha a kijelolt elemek kozott van requires_root.""" - if os.geteuid() == 0 or not any(rr for _, _, rr in paths): - return "", True - import time - if self._sudo_pw and (time.time() - self._sudo_pw_ts) < 300: - return self._sudo_pw, True - password = self._ask_password() - if not password: - return "", False - self._sudo_pw = password - self._sudo_pw_ts = time.time() - return password, True - - def _bk_set_busy(self, busy): - self.btn_bk_save.setEnabled(not busy) - self.btn_bk_restore.setEnabled(not busy) - self.dest_edit.setEnabled(not busy) - - def _do_backup(self): - paths = self._bk_selected() - if not paths: - self.bk_log.appendPlainText(tr("log_nothing_selected")) - return - dest = self.dest_edit.text().strip() - if not dest: - self.bk_log.appendPlainText(tr("bk_need_dest")) - return - password, ok = self._bk_password_if_needed(paths) - if not ok: - return - self.bk_log.clear() - self.bk_log.show() - self.bk_log.appendPlainText(tr("bk_start_backup", dest=dest)) - self.bk_bar.setValue(0) - self._bk_set_busy(True) - self.backup_worker = BackupWorker(paths, dest, restore=False, password=password) - self.backup_worker.log.connect(self.bk_log.appendPlainText) - self.backup_worker.progress.connect(self.bk_bar.setValue) - self.backup_worker.done.connect(self._bk_done) - self.backup_worker.start() - - def _do_restore(self): - dest = self.dest_edit.text().strip() - if not dest or not Path(dest).is_file(): - self.bk_log.appendPlainText(tr("bk_invalid_file")) - return - paths = self._bk_selected() - if not paths: - self.bk_log.appendPlainText(tr("bk_need_selection")) - return - password, ok = self._bk_password_if_needed(paths) - if not ok: - return - self.bk_log.clear() - self.bk_log.show() - self.bk_log.appendPlainText(tr("bk_start_restore", dest=dest)) - self.bk_bar.setValue(0) - self._bk_set_busy(True) - self.backup_worker = BackupWorker(paths, dest, restore=True, password=password) - self.backup_worker.log.connect(self.bk_log.appendPlainText) - self.backup_worker.progress.connect(self.bk_bar.setValue) - self.backup_worker.done.connect(self._bk_done) - self.backup_worker.start() - - def _bk_done(self, ok, msg): - self.bk_bar.setValue(100 if ok else 0) - self.bk_log.appendPlainText(f"\n{tr('bk_result_ok') if ok else tr('bk_result_err')}: {msg}") - self._bk_set_busy(False) - - if shutil.which("hyprctl"): - subprocess.run( - ["hyprctl", "keyword", "windowrulev2", - f"opacity 1.0 1.0, class:^({APP_ID})$"], - capture_output=True, - ) - - app = QApplication(sys.argv) - app.setApplicationName(APP_NAME) - app.setApplicationDisplayName(APP_NAME) - app.setDesktopFileName(APP_ID) - - try: - catalog = load_catalog() - except RuntimeError as exc: - QMessageBox.critical(None, APP_NAME, str(exc)) - return 1 - - win = Main(catalog) - win.setStyleSheet(build_qss()) - win.show() - return app.exec() - - - -def _resolve(names, catalog): - by = {} - for a in catalog: - by[a["name"].lower()] = a - if a.get("type") == "choice": - for o in a.get("options", []): - by.setdefault(o["name"].lower(), { - **o, - "type": "pacman", - "category": a["category"], - "name": f"{a['name']}: {o['name']}", - }) - found, missing = [], [] - for n in names: - a = by.get(n.lower()) - (found if a else missing).append(a if a else n) - return found, missing - - -def cli_list(catalog, only_installed=False): - idx = installed_index() - for a in catalog: - inst = is_installed(a, idx) - if only_installed and not inst: - continue - mark = "[x]" if inst else "[ ]" - print(f"{mark} {a['name']:<22} {a['category']:<10} " - f"{TYPE_LABEL.get(a['type'], a['type'])}") - - -def cli_action(names, catalog, install: bool): - found, missing = _resolve(names, catalog) - for m in missing: - print(f"Nem található: {m}") - if not found: - return 1 - idx = installed_index() - expanded = [] - for a in found: - if a.get("type") == "choice": - for o in a.get("options", []): - if install or _option_installed(o, idx): - expanded.append({ - **o, - "type": "pacman", - "category": a["category"], - "name": f"{a['name']}: {o['name']}", - }) - else: - expanded.append(a) - found = expanded - if not found: - print("Nincs végrehajtható lépés.") - return 1 - steps = plan_commands(found, install, gui=False) - if not steps: - print("Nincs végrehajtható lépés.") - return 1 - - password = "" - if os.geteuid() != 0 and any( - len(cmd) >= 2 and cmd[0] == "sudo" and cmd[1] == "-S" for _, cmd in steps - ): - import getpass - password = getpass.getpass("Sudo jelszó: ") - - ok = 0 - for label, cmd in steps: - print(f"\n── {label} ──") - if stream_command(label, cmd, print, password): - ok += 1 - print(f"\nKész: {ok}/{len(steps)} lépés sikeres.") - return 0 if ok == len(steps) else 1 - - -def main(): - parser = argparse.ArgumentParser( - prog=APP_ID, description="RaveOS App Installer (GUI + CLI)") - parser.add_argument("--list", action="store_true", help="elérhető appok listája") - parser.add_argument("--installed", action="store_true", help="csak telepítettek") - parser.add_argument("--install", nargs="+", metavar="APP", help="app(ok) telepítése") - parser.add_argument("--remove", nargs="+", metavar="APP", help="app(ok) eltávolítása") - parser.add_argument("--gui", action="store_true", help="GUI indítása (alapértelmezett)") - parser.add_argument("--version", action="version", version=f"%(prog)s {APP_VERSION}") - args = parser.parse_args() - - cli_mode = args.list or args.installed or args.install or args.remove - if not cli_mode: - if os.geteuid() == 0: - print("Figyelem: a GUI-t NE rootként indítsd. " - "Futtasd sima userként, a jogot pkexec kéri majd.") - try: - sys.exit(run_gui()) - except ModuleNotFoundError: - print("PyQt6 nincs telepítve. Telepítsd: sudo pacman -S python-pyqt6") - sys.exit(1) - - try: - catalog = load_catalog() - except RuntimeError as exc: - print(exc) - sys.exit(1) - - if args.list or args.installed: - cli_list(catalog, only_installed=args.installed) - sys.exit(0) - if args.install: - sys.exit(cli_action(args.install, catalog, install=True)) - if args.remove: - sys.exit(cli_action(args.remove, catalog, install=False)) - - -if __name__ == "__main__": - main() |