#!/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 = 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 # ───────────────────────────────────────────────────────────────────────────── # Konstansok / útvonalak # ───────────────────────────────────────────────────────────────────────────── APP_NAME = "RaveOS App Installer" APP_ID = "raveos-app-installer" # Wayland app_id (Hyprland window rule-okhoz) APP_VERSION = "3.1" # app-list.json keresési sorrend: a script mellett -> /usr/share -> ~/.config _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", ] # ───────────────────────────────────────────────────────────────────────────── # Téma (a welcome app stílusa) # ───────────────────────────────────────────────────────────────────────────── C = { "bg": "#232427", "header": "#1b1c1e", "surface": "#2b2c30", "surface_hi": "#34353a", "border": "#3a3b40", "border_hi": "#4a4b52", "accent": "#62a052", # muted natural green "accent_hi": "#72b062", "accent_dim": "#4d8043", "text": "#eaeaea", "text_dim": "#9298a0", "text_mut": "#6c727a", "danger": "#b5544a", "danger_hi": "#c56458", } # Per-type kis badge színek (visszafogottak) TYPE_LABEL = { "pacman": "pacman", "aur": "AUR", "flatpak": "flatpak", "flatpak-user": "flatpak", "script": "script", "message": "info", } # ───────────────────────────────────────────────────────────────────────────── # Nyelv (hu/en) — session-szintű, alapértelmezett hu, a welcome app mintájára # ───────────────────────────────────────────────────────────────────────────── _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"}, "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"}, "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']}; }} /* 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; }} """ # ───────────────────────────────────────────────────────────────────────────── # Adat: katalógus betöltés + telepítettség detektálás # ───────────────────────────────────────────────────────────────────────────── 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 return False # ───────────────────────────────────────────────────────────────────────────── # Parancs-építés (telepítés / eltávolítás), batch-elve backendenként # ───────────────────────────────────────────────────────────────────────────── 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: # --answer* nelkul a yay a PKGBUILD-diff/edit prompt-nal orokre # varakozik (nincs csatolt terminal a stream_command-hoz) 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])) # Config seedelés: pacman/aur/flatpak-user tipusu appoknal a config_dir/ # config_dest mezok alapjan bemasoljuk az elore elkeszitett configot a # user home-jaba, telepites utan. (script tipusnal ezt a sajat bash # script intezi, azoknal nem kell ez a lepes.) 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) steps.append((f"config: {a['name']}", ["bash", "-c", f"mkdir -p {shlex.quote(dest)} && " f"cp -rn {shlex.quote(str(src))}/. {shlex.quote(dest)}/"])) # Config seedelés (több-utas változat): olyan appoknal, ahol tobb kulon # forras/cel par van (pl. PhotoGIMP: GIMP config + .desktop + ikonok # kulon-kulon helyre), a "config_mappings" listat hasznaljuk a fenti # egyszeru config_dir/config_dest helyett. 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) steps.append((f"config: {a['name']} ({msrc})", ["bash", "-c", f"mkdir -p {shlex.quote(dest)} && " f"cp -rn {shlex.quote(str(src))}/. {shlex.quote(dest)}/"])) # Script env: a szkript tudja ki a tényleges user (sudo után root fut) _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 # ───────────────────────────────────────────────────────────────────────────── # GUI rész (csak ha van PyQt6 / display) # ───────────────────────────────────────────────────────────────────────────── 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, ) from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QFont, QFontDatabase # ── display font helper (Bebas Neue, fallbackkel) ──────────────────────── 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() # fallback: rendszer sans, bold + tracking f.setPointSize(size) f.setBold(True) f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 102) return f # ── háttér worker ──────────────────────────────────────────────────────── class Worker(QThread): log = pyqtSignal(str) progress = pyqtSignal(int) # 0..100 done = pyqtSignal(int, int) # ok_steps, total_steps 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) # ── egy app sor widget ─────────────────────────────────────────────────── class Row(QFrame): def __init__(self, app, installed): super().__init__() self.app = app self.installed = installed self.is_message = app.get("type") == "message" 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_message: status = QLabel(tr("row_info")) status.setStyleSheet(f"color:{C['accent_hi']}; font-size:12px;") 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) # sorra kattintás is váltja a checkboxot (message tipusnal dialogot mutat) self.setCursor(Qt.CursorShape.PointingHandCursor) 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) # ── backup worker ──────────────────────────────────────────────────────── 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 # [(label, abs_path, requires_root), ...] self.dest = dest # str: .tar.gz útvonal 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)) # ── fő ablak ───────────────────────────────────────────────────────────── 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.backup_worker = None self.backup_checkboxes = {} # item_id -> (QCheckBox, abs_path, label, requires_root) self._sudo_pw = "" self._sudo_pw_ts = 0.0 # timestamp: mikor lett megjegyezve 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 ─────────────────────────────────────────────────────── 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_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) # ── Stack ───────────────────────────────────────────────────────── self.stack = QStackedWidget() outer.addWidget(self.stack, 1) # ── Oldal 0: Apps ───────────────────────────────────────────────── 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) # ── Oldal 1: Backup / Restore ───────────────────────────────────── 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) self.build_rows() self.apply_filter() # ── nyelv ───────────────────────────────────────────────────────────── 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): on_backup = self.stack.currentIndex() == 1 self.header_sep.setText(tr("header_backup") if on_backup else tr("header_installer")) self.btn_nav.setText(tr("nav_back") if on_backup else tr("nav_backup")) 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() # ── backup lista építés (nyelvváltáskor is újraépül) ──────────────────── 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) # ── frissítés ───────────────────────────────────────────────────────── def _refresh_and_filter(self): self.idx = installed_index() self.build_rows() self.apply_filter() # ── navigáció ───────────────────────────────────────────────────────── 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) # ── lista építés ────────────────────────────────────────────────────── def build_rows(self): for r in self.rows: r.setParent(None) self.rows.clear() for app in self.catalog: inst = is_installed(app, self.idx) row = Row(app, 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 if only_inst and not r.installed: show = False if not only_inst and r.installed: show = False if cat_idx != 0 and r.app["category"] != cat: show = False if q and q not in r.app["name"].lower() \ and q not in _loc(r.app.get("desc")).lower(): show = False r.setVisible(show) def selected(self): return [r.app for r in self.rows if r.cb.isChecked() and r.isVisible() and not r.is_message] # ── app akció ───────────────────────────────────────────────────────── 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: # Ha az ellenőrzés nem sikerül, elfogadjuk és hagyjuk a parancsot hibázni 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 # Jelszó — 5 percen belül megjegyzett jelszót újra felhasználjuk 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) # ── backup akciók ───────────────────────────────────────────────────── 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) # ── alkalmazás bootstrap ───────────────────────────────────────────────── 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) # Wayland app_id -> Hyprland window rule 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() # ───────────────────────────────────────────────────────────────────────────── # CLI rész # ───────────────────────────────────────────────────────────────────────────── def _resolve(names, catalog): by = {a["name"].lower(): a for a in catalog} 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 steps = plan_commands(found, install, gui=False) if not steps: print("Nincs végrehajtható lépés.") return 1 # sudo -S jelszo nelkul a stdin pipe nyitva marad iras/EOF nelkul, # es a folyamat orokre lefagy - a GUI-nak van jelszo dialogja, a # CLI-nek eddig nem volt. 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: # GUI-t user-ként indítsd (Wayland) — root GUI ne! 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()