summaryrefslogtreecommitdiff
path: root/raveos-app-installer/raveos-app-installer.py
diff options
context:
space:
mode:
authorNippy <nippy@Nippy-Mac-mini.local>2026-11-01 18:13:54 +0100
committerNippy <nippy@Nippy-Mac-mini.local>2026-11-01 18:13:54 +0100
commita64ef966534f8dba6af889ba4bfce15691bfc85b (patch)
treeb30f284d05b80fa115569ab220deae13797fad6f /raveos-app-installer/raveos-app-installer.py
parent919f977b40016f588b0c8532c8a57f6035056b95 (diff)
downloadRaveOS-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.py1870
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()