summaryrefslogtreecommitdiff
path: root/raveos-welcome
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-welcome')
-rw-r--r--raveos-welcome/PKGBUILD39
-rwxr-xr-xraveos-welcome/gaming-optimize.sh255
-rw-r--r--raveos-welcome/hu.rp1.raveos-welcome.policy20
-rw-r--r--raveos-welcome/raveos-welcome.desktop12
-rw-r--r--raveos-welcome/raveos-welcome.svg18
-rw-r--r--raveos-welcome/raveos_welcome.py2215
6 files changed, 2559 insertions, 0 deletions
diff --git a/raveos-welcome/PKGBUILD b/raveos-welcome/PKGBUILD
new file mode 100644
index 0000000..a4319ec
--- /dev/null
+++ b/raveos-welcome/PKGBUILD
@@ -0,0 +1,39 @@
+pkgname=raveos-welcome
+pkgver=2.0.0
+pkgrel=6
+pkgdesc="RaveOS üdvözlő ablak - első bejelentkezéskor jelenik meg"
+arch=('any')
+url="https://git.rp1.hu/RaveOS/raveos-welcome"
+license=('GPL')
+depends=('python' 'python-pyqt6' 'polkit' 'networkmanager')
+source=()
+sha256sums=()
+
+BUILDDIR="${startdir}/build"
+PKGDEST="${startdir}/build"
+SRCDEST="${startdir}/build"
+SRCPKGDEST="${startdir}/build"
+LOGDEST="${startdir}/build"
+
+package() {
+ install -Dm755 "${startdir}/raveos_welcome.py" \
+ "${pkgdir}/usr/bin/raveos-welcome"
+
+ install -Dm644 "${startdir}/raveos-welcome.desktop" \
+ "${pkgdir}/etc/xdg/autostart/raveos-welcome.desktop"
+
+ install -Dm644 "${startdir}/raveos-welcome.svg" \
+ "${pkgdir}/usr/share/raveos-welcome/raveos-welcome.svg"
+
+ install -Dm755 "${startdir}/gaming-optimize.sh" \
+ "${pkgdir}/usr/share/raveos-welcome/gaming-optimize.sh"
+
+ install -Dm644 "${startdir}/hu.rp1.raveos-welcome.policy" \
+ "${pkgdir}/usr/share/polkit-1/actions/hu.rp1.raveos-welcome.policy"
+
+ if [ -d "${startdir}/brave-profile" ]; then
+ cp -r "${startdir}/brave-profile" "${pkgdir}/usr/share/raveos-welcome/brave-profile"
+ find "${pkgdir}/usr/share/raveos-welcome/brave-profile" -type d -exec chmod 755 {} +
+ find "${pkgdir}/usr/share/raveos-welcome/brave-profile" -type f -exec chmod 644 {} +
+ fi
+}
diff --git a/raveos-welcome/gaming-optimize.sh b/raveos-welcome/gaming-optimize.sh
new file mode 100755
index 0000000..1c23d18
--- /dev/null
+++ b/raveos-welcome/gaming-optimize.sh
@@ -0,0 +1,255 @@
+#!/bin/bash
+# RaveOS Gaming Optimization Helper — fut root-ként pkexec-en keresztül
+set -uo pipefail
+
+LOGDIR="/var/log/raveos-welcome"
+LOGFILE="$LOGDIR/gaming-optimize.log"
+mkdir -p "$LOGDIR" 2>/dev/null || true
+exec > >(tee -a "$LOGFILE") 2>&1
+echo "=== $(date '+%Y-%m-%d %H:%M:%S') optimize run: $* ==="
+
+apply_sysctl() {
+ cat > /etc/sysctl.d/99-gaming.conf << 'EOF'
+vm.swappiness=10
+vm.compaction_proactiveness=0
+vm.watermark_boost_factor=1
+vm.page_lock_unfairness=1
+vm.dirty_writeback_centisecs=500
+kernel.split_lock_mitigate=0
+kernel.nmi_watchdog=0
+net.ipv4.tcp_fastopen=3
+net.core.netdev_max_backlog=4096
+vm.max_map_count=2147483642
+EOF
+ sysctl --load=/etc/sysctl.d/99-gaming.conf >/dev/null 2>&1
+
+ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true
+ cat > /etc/tmpfiles.d/thp-gaming.conf << 'EOF'
+w /sys/kernel/mm/transparent_hugepage/enabled - - - - madvise
+EOF
+
+ if [ "$(sysctl -n vm.swappiness 2>/dev/null)" != "10" ]; then
+ echo "[FAIL] sysctl: vm.swappiness nem allt be, a sysctl --load hibazott"
+ return 1
+ fi
+ echo "[OK] sysctl alkalmazva"
+}
+
+apply_io_sched() {
+ cat > /etc/udev/rules.d/60-ssd-scheduler.rules << 'EOF'
+ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
+ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
+ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
+EOF
+ udevadm control --reload-rules 2>/dev/null || true
+ udevadm trigger --subsystem-match=block --action=change 2>/dev/null || true
+
+ # azonnali alkalmazas is, ne csak legkozelebbi boot/hotplug-kor
+ for sched in /sys/block/*/queue/scheduler; do
+ [ -f "$sched" ] || continue
+ dev="${sched%/queue/scheduler}"
+ dev="${dev##*/}"
+ case "$dev" in
+ nvme*)
+ echo none > "$sched" 2>/dev/null || true
+ ;;
+ sd*)
+ rot_file="${sched%scheduler}rotational"
+ if [ "$(cat "$rot_file" 2>/dev/null)" = "0" ]; then
+ echo mq-deadline > "$sched" 2>/dev/null || true
+ else
+ echo bfq > "$sched" 2>/dev/null || true
+ fi
+ ;;
+ esac
+ done
+ echo "[OK] io_sched alkalmazva"
+}
+
+apply_ananicy() {
+ if ! pacman -Q ananicy-cpp &>/dev/null && ! pacman -Q ananicy-cpp-git &>/dev/null; then
+ if ! pacman -S --noconfirm --needed ananicy-cpp cachyos-ananicy-rules; then
+ echo "[FAIL] ananicy: pacman telepites sikertelen"
+ return 1
+ fi
+ fi
+ if ! systemctl enable --now ananicy-cpp; then
+ echo "[FAIL] ananicy: systemctl enable --now sikertelen"
+ return 1
+ fi
+ echo "[OK] ananicy alkalmazva"
+}
+
+apply_gamemode() {
+ if ! pacman -S --noconfirm --needed gamemode lib32-gamemode mangohud lib32-mangohud goverlay; then
+ echo "[FAIL] gamemode: pacman telepites sikertelen (multilib repo engedelyezve van?)"
+ return 1
+ fi
+ cat > /etc/gamemode.ini << 'EOF'
+[general]
+renice=10
+softrealtime=auto
+inhibit_screensaver=1
+
+[cpu]
+park_cores=no
+pin_cores=yes
+
+[gpu]
+apply_gpu_optimisations=accept-responsibility
+gpu_device=0
+amd_performance_level=high
+EOF
+ echo "[OK] gamemode alkalmazva"
+}
+
+apply_gpu_profile() {
+ local tmpfile="/etc/tmpfiles.d/gpu-power-profile.conf"
+ > "$tmpfile"
+ local applied=0
+ for card in /sys/class/drm/card*/device/pp_power_profile_mode; do
+ [ -f "$card" ] || continue
+ local idx
+ idx=$(awk '{name=$2; gsub(/[:*]/,"",name); if (name=="3D_FULL_SCREEN") print $1}' "$card" 2>/dev/null | head -1)
+ if [ -n "$idx" ] && echo "$idx" > "$card" 2>/dev/null; then
+ echo "w $card - - - - $idx" >> "$tmpfile"
+ applied=1
+ fi
+ done
+ if [ "$applied" -eq 0 ]; then
+ echo "[FAIL] gpu_profile: nincs elerheto 3D_FULL_SCREEN profil (pp_power_profile_mode)"
+ return 1
+ fi
+ echo "[OK] gpu_profile alkalmazva"
+}
+
+apply_amd_powercap() {
+ local tmpfile="/etc/tmpfiles.d/amd-powercap.conf"
+ > "$tmpfile"
+ local applied=0
+ for cap_max in /sys/class/hwmon/hwmon*/power1_cap_max; do
+ [ -f "$cap_max" ] || continue
+ local cap_file="${cap_max%_max}"
+ local max_val
+ max_val=$(cat "$cap_max" 2>/dev/null) || continue
+ if [ -n "$max_val" ] && echo "$max_val" > "$cap_file" 2>/dev/null; then
+ echo "w $cap_file - - - - $max_val" >> "$tmpfile"
+ applied=1
+ fi
+ done
+ if [ "$applied" -eq 0 ]; then
+ echo "[FAIL] amd_powercap: nem talalhato irhato power1_cap"
+ return 1
+ fi
+ echo "[OK] amd_powercap alkalmazva"
+}
+
+apply_amd_overdrive() {
+ echo "options amdgpu ppfeaturemask=0xffffffff" > /etc/modprobe.d/99-amdgpu-overdrive.conf
+ if ! mkinitcpio -P; then
+ echo "[FAIL] amd_overdrive: mkinitcpio -P sikertelen"
+ return 1
+ fi
+ echo "[OK] amd_overdrive alkalmazva (ujrainditas szukseges)"
+}
+
+apply_nvidia_perf() {
+ local ok=1
+
+ if command -v nvidia-smi &>/dev/null; then
+ nvidia-smi -pm 1 >/dev/null 2>&1 || echo "[WARN] nvidia_perf: persistence mode sikertelen"
+ # --auto-boost-default csak regi Kepler karyakon letezik, modern GPU-n
+ # normalis hogy hibazik, ezert ez nem szamit kritikus hibanak
+ nvidia-smi --auto-boost-default=0 >/dev/null 2>&1 || true
+ else
+ echo "[FAIL] nvidia_perf: nvidia-smi nem talalhato"
+ ok=0
+ fi
+
+ # a regi verzio /proc/driver/nvidia/params-ba irt tmpfiles-szel, ami
+ # csak-olvashato es sosem mukodott - modul parametert modprobe.d-vel
+ # kell beallitani, modul betoltes elott
+ rm -f /etc/tmpfiles.d/nvidia-perf.conf
+ echo "options nvidia NVreg_EnableGpuFirmware=0" > /etc/modprobe.d/99-nvidia-gaming.conf
+ if ! mkinitcpio -P; then
+ echo "[FAIL] nvidia_perf: mkinitcpio -P sikertelen"
+ ok=0
+ fi
+
+ if [ "$ok" -ne 1 ]; then
+ return 1
+ fi
+ echo "[OK] nvidia_perf alkalmazva (firmware tiltas ujrainditas utan lep eletbe)"
+}
+
+apply_brave() {
+ # brave-origin-bin kesz binariskent elerheto a chaotic-aur repoban,
+ # nincs szukseg yay/AUR buildre (ami korabban lefagyott a PKGBUILD-diff
+ # promptnal, mert a --noconfirm azt nem nemitja el).
+ if pacman -S --noconfirm --needed brave-origin-bin; then
+ echo "[OK] brave-origin telepitve"
+ return 0
+ fi
+ echo "[FAIL] brave-origin: pacman telepites sikertelen"
+ return 1
+}
+
+restore_brave_profile() {
+ # Brave Origin a binarisba egetett "BraveSoftware/Brave-Origin" mappat
+ # hasznalja (nem "Brave-Browser", mint a rendes Brave) - strings-szel
+ # ellenorizve az /opt/brave-origin-bin/brave binariban.
+ local src="/usr/share/raveos-welcome/brave-profile/config/BraveSoftware/Brave-Browser"
+ if [ ! -d "$src" ]; then
+ echo "[FAIL] brave_profile: forras profil nem talalhato ($src)"
+ return 1
+ fi
+ local real_user
+ real_user="$(logname 2>/dev/null || echo "${SUDO_USER:-}")"
+ if [ -z "$real_user" ]; then
+ echo "[FAIL] brave_profile: nem sikerult megallapitani a felhasznalot"
+ return 1
+ fi
+ local home_dir
+ home_dir="$(getent passwd "$real_user" | cut -d: -f6)"
+ local dest="$home_dir/.config/BraveSoftware/Brave-Origin"
+ if ! { mkdir -p "$dest" && cp -a "$src/." "$dest/" && chown -R "$real_user":"$real_user" "$dest"; }; then
+ echo "[FAIL] brave_profile: masolas sikertelen"
+ return 1
+ fi
+ echo "[OK] brave_profile visszaallitva"
+}
+
+apply_firefox() {
+ if ! pacman -S --noconfirm --needed firefox; then
+ echo "[FAIL] firefox: pacman telepites sikertelen"
+ return 1
+ fi
+ echo "[OK] firefox telepitve"
+}
+
+FAILED=()
+for arg in "$@"; do
+ case "$arg" in
+ sysctl) apply_sysctl || FAILED+=("$arg") ;;
+ io_sched) apply_io_sched || FAILED+=("$arg") ;;
+ ananicy) apply_ananicy || FAILED+=("$arg") ;;
+ gamemode) apply_gamemode || FAILED+=("$arg") ;;
+ gpu_profile) apply_gpu_profile || FAILED+=("$arg") ;;
+ amd_powercap) apply_amd_powercap || FAILED+=("$arg") ;;
+ amd_overdrive) apply_amd_overdrive || FAILED+=("$arg") ;;
+ nvidia_perf) apply_nvidia_perf || FAILED+=("$arg") ;;
+ brave) apply_brave || FAILED+=("$arg") ;;
+ brave_profile) restore_brave_profile || FAILED+=("$arg") ;;
+ firefox) apply_firefox || FAILED+=("$arg") ;;
+ *) echo "[WARN] ismeretlen opcio: $arg" ;;
+ esac
+done
+
+if [ "${#FAILED[@]}" -gt 0 ]; then
+ echo "HIBA: a kovetkezok nem alkalmazodtak sikeresen: ${FAILED[*]}" >&2
+ echo "Naplo: $LOGFILE" >&2
+ exit 1
+fi
+
+echo "Minden kivalasztott optimalizacio sikeresen alkalmazva."
+exit 0
diff --git a/raveos-welcome/hu.rp1.raveos-welcome.policy b/raveos-welcome/hu.rp1.raveos-welcome.policy
new file mode 100644
index 0000000..c049481
--- /dev/null
+++ b/raveos-welcome/hu.rp1.raveos-welcome.policy
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+<policyconfig>
+ <action id="hu.rp1.raveos-welcome">
+ <description>Run RaveOS Welcome gaming optimization</description>
+ <description xml:lang="hu">RaveOS Üdvözlő gaming optimalizáció futtatása</description>
+ <message>Authentication is required to apply gaming optimizations</message>
+ <message xml:lang="hu">Hitelesítés szükséges a gaming optimalizáció alkalmazásához</message>
+ <icon_name>preferences-system</icon_name>
+ <defaults>
+ <allow_any>auth_admin_keep</allow_any>
+ <allow_inactive>auth_admin_keep</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.exec.path">/usr/share/raveos-welcome/gaming-optimize.sh</annotate>
+ <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
+ </action>
+</policyconfig>
diff --git a/raveos-welcome/raveos-welcome.desktop b/raveos-welcome/raveos-welcome.desktop
new file mode 100644
index 0000000..18a24b0
--- /dev/null
+++ b/raveos-welcome/raveos-welcome.desktop
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=RaveOS Welcome
+Comment=RaveOS üdvözlő ablak
+Exec=env QT_QPA_PLATFORM=wayland;xcb QT_STYLE_OVERRIDE=kvantum /usr/bin/raveos-welcome
+Icon=raveos-welcome
+Terminal=false
+Type=Application
+Categories=System;
+X-GNOME-Autostart-enabled=true
+X-KDE-autostart-condition=raveos-welcome
+Hidden=false
+NoDisplay=true
diff --git a/raveos-welcome/raveos-welcome.svg b/raveos-welcome/raveos-welcome.svg
new file mode 100644
index 0000000..49a9777
--- /dev/null
+++ b/raveos-welcome/raveos-welcome.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Creator: CorelDRAW 2018 (64-Bit) -->
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="34px" height="32px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
+viewBox="0 0 34 32.01"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <style type="text/css">
+ <![CDATA[
+ .fil0 {fill:#FEFEFE}
+ ]]>
+ </style>
+ </defs>
+ <g id="Layer_x0020_1">
+ <metadata id="CorelCorpID_0Corel-Layer"/>
+ <path class="fil0" d="M28.02 7.51c0.91,0.24 1.69,0.55 2.13,0.9 0.85,-0.35 1.53,-0.65 2.01,-1.22 -1.13,0.37 -2.62,0.35 -4.14,0.32zm-4.48 4.75c-0.51,0.11 -2.39,1.09 -3.15,2.53 0.78,0.76 1.85,1.01 2.78,0.38 1.04,-0.79 1.27,-1.78 0.37,-2.91zm-3.42 12.13c-0.43,1.64 -2.41,3.33 -2.78,4.37 -0.01,0.8 -0.01,2.46 -0.02,3.25 0.8,-2.83 3.19,-4.36 3.41,-6.12 -0.21,-0.41 -0.5,-1.01 -0.61,-1.5zm-0.53 -1.03c-0.54,1.98 -1.46,2.07 -2.25,3.78 0,-1.22 0,-1.39 0,-2.61 0.23,-0.94 0.86,-2.66 1.69,-3.9 -0.01,0.8 0.28,1.74 0.56,2.73zm5.86 -2.02c0.46,0.73 0.93,2.1 1.04,2.95 0.74,0.73 1.02,1.68 1.26,2.65 0.11,-1.51 -0.07,-2.91 -0.62,-4.16 -0.48,-0.58 -1.21,-1.15 -1.68,-1.44zm-2.23 -1.95c-0.76,0.25 -2.37,0.5 -3.31,0.38 0.22,2.24 1.68,5.64 3.53,7.35 -0.68,-1.94 -0.83,-3.8 -0.99,-4.64 1.66,1.62 2.86,4.8 3.52,8.18 0.2,-3.22 0.2,-8.42 -2.75,-11.27zm4.29 -2.49c-0.96,1.26 -2.17,1.76 -3.53,2.27 3.69,2.48 4.35,3.53 5.14,4.71 -0.26,-1.55 -0.54,-3.1 -1.61,-4.51 1.72,0.09 3.56,0.94 5.45,2.06 -1.17,-2.07 -2.69,-3.84 -5.45,-4.53zm1.02 -4.97c1.13,0.29 3.01,1.39 3.81,2.06 -1.37,-0.11 -2.52,0.03 -3.16,0.29 1.94,0.97 3.78,3.07 4.22,4.28 -0.74,-0.68 -3.14,-2.21 -5.57,-2.34 0.61,-0.77 0.97,-2.52 0.7,-4.29zm-0.67 -0.31c-0.31,4.11 -2.74,5.69 -6.59,5.6 -0.12,0.44 -0.62,1.29 -1.33,1.86 3.38,-0.07 6.25,-1.09 7.43,-3.34 0.54,-1.03 0.92,-2.49 0.49,-4.12zm-8.18 -7.47c-0.42,1.68 -2.21,2.65 -2.64,4.28l0 0.02 -0.01 -0.01 0 0.01 0 -0.02c-0.43,-1.63 -2.22,-2.6 -2.64,-4.28 0.48,-1.17 1.29,-1.97 2.64,-1.95 1.36,-0.02 2.17,0.78 2.65,1.95zm3.4 2.61c0.26,-0.8 0.58,-2.03 0.58,-2.81 0.8,-0.85 2.62,-1.7 3.42,-2.12 -1.31,1.37 -3.53,3.78 -4,4.93zm0.11 0.61c1.15,-0.3 2.45,-0.27 3.57,-0.15 1.6,-1.04 2.5,-2.35 3.38,-3.66 -1.93,1.72 -5.55,1.55 -6.95,3.81zm-5.8 2.17c-0.01,-1.93 4.49,-3.92 4.97,-9.54 0.67,2.22 0.99,4.6 -0.07,7.62 -1.48,0.48 -2.84,1.29 -3.74,2.12 -0.43,0.55 -0.95,1.79 -1.16,2.83 0,-0.73 0,-2.29 0,-3.03zm0.79 3.93c0.36,-1.32 2.04,-2.48 3.49,-3.02 2.89,-1.07 7.82,-0.18 12.33,1.39 -2.31,-1.89 -4.7,-3.32 -6.98,-3.77 -2.93,-0.14 -5.63,0.07 -7.91,2.3 -0.52,0.78 -0.89,1.85 -0.93,3.1zm1.07 1.02c-0.09,-0.22 -0.2,-0.75 -0.2,-1.02 0.87,-2.09 3.69,-2.63 5.49,-2.91 -2.92,0.9 -4.87,2.68 -5.29,3.93zm-2.22 7.33c-1.28,-3.26 -2.12,-2.47 -2.79,-4.85 1.23,-0.64 1.8,-1.92 2.01,-2.89 0.26,-0.02 0.57,-0.12 0.77,-0.25 0.2,0.13 0.52,0.23 0.77,0.25 0.21,0.97 0.79,2.25 2.02,2.89 -0.68,2.38 -1.51,1.59 -2.78,4.85zm-11.05 -14.31c-0.91,0.24 -1.69,0.55 -2.13,0.9 -0.85,-0.35 -1.53,-0.65 -2.01,-1.22 1.13,0.37 2.62,0.35 4.14,0.32zm4.48 4.75c0.51,0.11 2.39,1.09 3.15,2.53 -0.78,0.76 -1.85,1.01 -2.78,0.38 -1.04,-0.79 -1.27,-1.78 -0.37,-2.91zm3.42 12.13c0.43,1.64 2.41,3.33 2.78,4.37 0.01,0.8 0.01,2.46 0.02,3.25 -0.8,-2.83 -3.19,-4.36 -3.41,-6.12 0.21,-0.41 0.5,-1.01 0.61,-1.5zm0.53 -1.03c0.54,1.98 1.46,2.07 2.25,3.78 0,-1.22 0,-1.39 0,-2.61 -0.23,-0.94 -0.86,-2.66 -1.69,-3.9 0.01,0.8 -0.28,1.74 -0.56,2.73zm-5.86 -2.02c-0.46,0.73 -0.93,2.1 -1.04,2.95 -0.74,0.73 -1.02,1.68 -1.26,2.65 -0.11,-1.51 0.07,-2.91 0.62,-4.16 0.48,-0.58 1.21,-1.15 1.68,-1.44zm2.23 -1.95c0.76,0.25 2.37,0.5 3.31,0.38 -0.22,2.24 -1.68,5.64 -3.53,7.35 0.68,-1.94 0.83,-3.8 0.99,-4.64 -1.66,1.62 -2.86,4.8 -3.52,8.18 -0.2,-3.22 -0.2,-8.42 2.75,-11.27zm-4.29 -2.49c0.96,1.26 2.17,1.76 3.53,2.27 -3.69,2.48 -4.35,3.53 -5.14,4.71 0.26,-1.55 0.54,-3.1 1.61,-4.51 -1.72,0.09 -3.56,0.94 -5.45,2.06 1.17,-2.07 2.69,-3.84 5.45,-4.53zm-1.02 -4.97c-1.13,0.29 -3.01,1.39 -3.81,2.06 1.37,-0.11 2.52,0.03 3.16,0.29 -1.94,0.97 -3.78,3.07 -4.22,4.28 0.74,-0.68 3.14,-2.21 5.57,-2.34 -0.61,-0.77 -0.97,-2.52 -0.7,-4.29zm0.67 -0.31c0.31,4.11 2.74,5.69 6.59,5.6 0.12,0.44 0.62,1.29 1.33,1.86 -3.38,-0.07 -6.25,-1.09 -7.43,-3.34 -0.54,-1.03 -0.92,-2.49 -0.49,-4.12zm4.78 -4.86c-0.26,-0.8 -0.58,-2.03 -0.58,-2.81 -0.8,-0.85 -2.62,-1.7 -3.42,-2.12 1.31,1.37 3.53,3.78 4,4.93zm-0.11 0.61c-1.15,-0.3 -2.45,-0.27 -3.57,-0.15 -1.6,-1.04 -2.5,-2.35 -3.38,-3.66 1.93,1.72 5.55,1.55 6.95,3.81zm5.8 2.17c0.01,-1.93 -4.49,-3.92 -4.97,-9.54 -0.67,2.22 -0.99,4.6 0.07,7.62 1.48,0.48 2.84,1.29 3.74,2.12 0.43,0.55 0.95,1.79 1.16,2.83 0,-0.73 0,-2.29 0,-3.03zm-0.79 3.93c-0.36,-1.32 -2.04,-2.48 -3.49,-3.02 -2.89,-1.07 -7.82,-0.18 -12.33,1.39 2.31,-1.89 4.7,-3.32 6.98,-3.77 2.93,-0.14 5.63,0.07 7.91,2.3 0.52,0.78 0.89,1.85 0.93,3.1zm-1.07 1.02c0.09,-0.22 0.2,-0.75 0.2,-1.02 -0.87,-2.09 -3.69,-2.63 -5.49,-2.91 2.92,0.9 4.87,2.68 5.29,3.93z"/>
+ </g>
+</svg>
diff --git a/raveos-welcome/raveos_welcome.py b/raveos-welcome/raveos_welcome.py
new file mode 100644
index 0000000..afe9fda
--- /dev/null
+++ b/raveos-welcome/raveos_welcome.py
@@ -0,0 +1,2215 @@
+#!/usr/bin/env python3
+"""
+RaveOS Welcome App — First-run onboarding wizard
+Requires: PyQt6, nmcli (NetworkManager)
+"""
+
+import os
+import re
+import sys
+import subprocess
+import shutil
+from pathlib import Path
+
+from PyQt6.QtWidgets import (
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QLabel, QPushButton, QStackedWidget, QLineEdit, QListWidget,
+ QListWidgetItem, QFrame, QGraphicsOpacityEffect, QProgressBar,
+ QSizePolicy, QDialog, QCheckBox, QScrollArea, QFileDialog
+)
+from PyQt6.QtCore import (
+ Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation,
+ QEasingCurve, QPoint, QSize, pyqtProperty, QObject, QProcess
+)
+from PyQt6.QtGui import (
+ QFont, QFontDatabase, QColor, QPalette, QLinearGradient,
+ QGradient, QPainter, QBrush, QPen, QPixmap, QIcon,
+ QFontMetrics, QPainterPath
+)
+from PyQt6.QtWidgets import QProxyStyle, QStyle
+from PyQt6.QtSvgWidgets import QSvgWidget
+
+# ─── Constants ────────────────────────────────────────────────────────────────
+
+FLAG_FILE = Path.home() / ".config" / "raveos" / ".welcome_done"
+APP_INSTALLER = "/usr/bin/raveos-app-installer"
+SVG_LOGO = "/usr/share/raveos-welcome/raveos-welcome.svg"
+_SCRIPT_DIR = Path(__file__).resolve().parent
+
+COLORS = {
+ "bg": "#2B2B2B",
+ "bg2": "#1e1e1e",
+ "surface": "#363636",
+ "surface2": "#404040",
+ "border": "#555555",
+ "accent": "#3d7839",
+ "accent_hover": "#285226",
+ "accent_light": "#5aa856",
+ "text": "#ecf0f1",
+ "text_dim": "#95a5a6",
+ "success": "#3d7839",
+ "warning": "#f39c12",
+ "error": "#e74c3c",
+}
+
+# ─── Translations ─────────────────────────────────────────────────────────────
+
+_current_lang = "hu"
+
+T = {
+ # Nav
+ "nav_back": {"hu": "← Vissza", "en": "← Back"},
+ "nav_next": {"hu": "Tovább →", "en": "Next →"},
+ # Step bar
+ "step_welcome": {"hu": "ÜDVÖZLÉS", "en": "WELCOME"},
+ "step_network": {"hu": "HÁLÓZAT", "en": "NETWORK"},
+ "step_optimize": {"hu": "OPTIMALIZÁCIÓ", "en": "OPTIMIZE"},
+ "step_hyprland": {"hu": "HYPRLAND", "en": "HYPRLAND"},
+ "step_credits": {"hu": "KÖZREMŰKÖDŐK", "en": "CREDITS"},
+ "step_finish": {"hu": "BEFEJEZÉS", "en": "FINISH"},
+ # Welcome page
+ "welcome_tag": {"hu": "LINUX DISTRIBUTION", "en": "LINUX DISTRIBUTION"},
+ "welcome_about": {"hu": "RaveOS finomhangolás, kulcsra kész állapot!",
+ "en": "RaveOS tuned, ready to go!"},
+ "btn_support": {"hu": "€ Támogatás", "en": "€ Support"},
+ "feat_perf": {"hu": "CachyOS kernel\noptimalizált teljesítmény",
+ "en": "CachyOS kernel\noptimized performance"},
+ "feat_gaming": {"hu": "Gaming-ready\nProton, MangoHud, GameMode",
+ "en": "Gaming-ready\nProton, MangoHud, GameMode"},
+ "feat_apps": {"hu": "App Installer\negy kattintás, minden szoftver",
+ "en": "App Installer\none click, all software"},
+ # WiFi page
+ "wifi_title": {"hu": "HÁLÓZAT", "en": "NETWORK"},
+ "wifi_subtitle": {"hu": "WiFi kapcsolat", "en": "WiFi connection"},
+ "wifi_scan": {"hu": "Frissítés", "en": "Refresh"},
+ "wifi_password": {"hu": "Jelszó:", "en": "Password:"},
+ "wifi_connect": {"hu": "CSATLAKOZÁS", "en": "CONNECT"},
+ "wifi_scanning": {"hu": "Hálózatok keresése...","en": "Scanning networks..."},
+ "wifi_none": {"hu": "Nem találtam hálózatot.","en": "No networks found."},
+ "wifi_found": {"hu": "{n} hálózat található","en": "{n} networks found"},
+ "wifi_open": {"hu": " [nyílt]", "en": " [open]"},
+ "wifi_encrypted": {"hu": " [titkosított]", "en": " [encrypted]"},
+ "wifi_selected": {"hu": "Kiválasztva: {ssid}", "en": "Selected: {ssid}"},
+ "wifi_connecting": {"hu": "Csatlakozás folyamatban...","en": "Connecting..."},
+ "wifi_connected": {"hu": "Csatlakozva: {ssid}", "en": "Connected: {ssid}"},
+ "wifi_error": {"hu": "Hiba: {err}", "en": "Error: {err}"},
+ "wifi_timeout": {"hu": "Időtúllépés — próbáld újra","en": "Timeout — try again"},
+ "wifi_unknown_err": {"hu": "Ismeretlen hiba", "en": "Unknown error"},
+ # Optimize page
+ "opt_title": {"hu": "ALKALMAZÁSOK & OPTIMALIZÁCIÓ", "en": "APPS & OPTIMIZATION"},
+ "opt_browser": {"hu": "BÖNGÉSZŐ", "en": "BROWSER"},
+ "opt_brave": {"hu": "Brave Origin", "en": "Brave Origin"},
+ "opt_brave_desc": {"hu": "Brave Origin — a Brave letisztított, reklám/rewards/kripto-mentes verziója",
+ "en": "Brave Origin — Brave's stripped-down version, without ads/rewards/crypto"},
+ "opt_firefox": {"hu": "Firefox", "en": "Firefox"},
+ "opt_firefox_desc": {"hu": "Mozilla Firefox — nyílt forrású, privacy-fókuszú",
+ "en": "Mozilla Firefox — open source, privacy-focused"},
+ "opt_brave_profile": {"hu": "Brave profil visszaállítása", "en": "Restore Brave profile"},
+ "opt_brave_profile_desc": {"hu": "Előre konfigurált RaveOS Brave profil másolása (könyvjelzők, beállítások)",
+ "en": "Copy pre-configured RaveOS Brave profile (bookmarks, settings)"},
+ "opt_gaming": {"hu": "GAMING OPTIMALIZÁCIÓ — GPU: {gpu}", "en": "GAMING OPTIMIZATION — GPU: {gpu}"},
+ "opt_sysctl": {"hu": "Kernel/sysctl tweaks",
+ "en": "Kernel/sysctl tweaks"},
+ "opt_sysctl_desc": {"hu": "Kernel paraméterek optimalizálása gaming-hoz:\n"
+ "• vm.swappiness=10 — RAM preferálása swap helyett\n"
+ "• THP=madvise — Transparent Huge Pages, kevesebb micro-stutter\n"
+ "• NMI watchdog kikapcsolva — kevesebb CPU interrupt overhead\n"
+ "• split_lock_mitigate=0 — egyes játékok gyorsabban futnak\n"
+ "• TCP FastOpen — gyorsabb hálózati kapcsolatok online játékhoz\n"
+ "Permanensen mentve: /etc/sysctl.d/99-gaming.conf",
+ "en": "Kernel parameter optimization for gaming:\n"
+ "• vm.swappiness=10 — prefer RAM over swap\n"
+ "• THP=madvise — Transparent Huge Pages, less micro-stutter\n"
+ "• NMI watchdog off — less CPU interrupt overhead\n"
+ "• split_lock_mitigate=0 — some games run faster\n"
+ "• TCP FastOpen — faster network for online gaming\n"
+ "Saved permanently: /etc/sysctl.d/99-gaming.conf"},
+ "opt_io_sched": {"hu": "I/O Scheduler",
+ "en": "I/O Scheduler"},
+ "opt_io_sched_desc":{"hu": "Lemez olvasás/írás ütemező optimalizálása:\n"
+ "• SSD → mq-deadline: alacsonyabb latency mint BFQ\n"
+ "• NVMe → none: NVMe saját belső ütemezőt használ, külső felesleges\n"
+ "• HDD → bfq: legjobb forgódobos meghajtóhoz\n"
+ "Permanensen mentve: /etc/udev/rules.d/60-ssd-scheduler.rules",
+ "en": "Disk I/O scheduler optimization:\n"
+ "• SSD → mq-deadline: lower latency than BFQ\n"
+ "• NVMe → none: NVMe uses its own internal scheduler\n"
+ "• HDD → bfq: best for rotational drives\n"
+ "Saved permanently: /etc/udev/rules.d/60-ssd-scheduler.rules"},
+ "opt_ananicy": {"hu": "ananicy-cpp",
+ "en": "ananicy-cpp"},
+ "opt_ananicy_desc": {"hu": "Automatikus process prioritás kezelő:\n"
+ "• Játékok indításkor magasabb CPU prioritást kapnak\n"
+ "• Háttérfolyamatok (frissítők, indexelők) alacsonyabb prioritást kapnak\n"
+ "• Eredmény: stabilabb FPS, kevesebb frame drop\n"
+ "Telepíti: ananicy-cpp + cachyos-ananicy-rules, majd engedélyezi",
+ "en": "Automatic process priority handler:\n"
+ "• Games get higher CPU priority on launch\n"
+ "• Background processes (updaters, indexers) get lower priority\n"
+ "• Result: more stable FPS, fewer frame drops\n"
+ "Installs: ananicy-cpp + cachyos-ananicy-rules, then enables"},
+ "opt_gamemode": {"hu": "GameMode + MangoHud",
+ "en": "GameMode + MangoHud"},
+ "opt_gamemode_desc":{"hu": "GameMode: automatikus teljesítmény-boost játék közben:\n"
+ "• CPU governor powersave → performance játék indításkor\n"
+ "• Játék bezárásakor visszavált powersave-re\n"
+ "• I/O és process prioritás is optimalizálódik\n\n"
+ "MangoHud: in-game overlay:\n"
+ "• FPS, frametime, GPU/CPU hőmérséklet, VRAM kijelzés\n"
+ "• GOverlay: grafikus felület a MangoHud beállításához\n"
+ "• Steam launch options: MANGOHUD=1 gamemoderun %command%",
+ "en": "GameMode: automatic performance boost while gaming:\n"
+ "• CPU governor powersave → performance on game launch\n"
+ "• Reverts to powersave when game closes\n"
+ "• I/O and process priority also optimized\n\n"
+ "MangoHud: in-game overlay:\n"
+ "• FPS, frametime, GPU/CPU temp, VRAM display\n"
+ "• GOverlay: GUI to configure MangoHud\n"
+ "• Steam launch options: MANGOHUD=1 gamemoderun %command%"},
+ "opt_gpu_profile": {"hu": "GPU Power Profile — 3D Full Screen",
+ "en": "GPU Power Profile — 3D Full Screen"},
+ "opt_gpu_profile_desc": {"hu": "AMDGPU teljesítmény profil átváltása 3D_FULL_SCREEN-re:\n"
+ "• Agresszívebb GPU órajel játék közben\n"
+ "• Alapértelmezett profil visszafogottabb teljesítményre törekszik\n"
+ "• Eredmény: magasabb és stabilabb FPS\n"
+ "Permanensen mentve: /etc/tmpfiles.d/gpu-power-profile.conf",
+ "en": "AMDGPU power profile switch to 3D_FULL_SCREEN:\n"
+ "• More aggressive GPU clocks while gaming\n"
+ "• Default profile is more conservative\n"
+ "• Result: higher and more stable FPS\n"
+ "Saved permanently: /etc/tmpfiles.d/gpu-power-profile.conf"},
+ "opt_amd_overdrive": {"hu": "AMD Overdrive (ppfeaturemask)",
+ "en": "AMD Overdrive (ppfeaturemask)"},
+ "opt_amd_overdrive_desc": {"hu": "AMD GPU Overdrive funkciók feloldása:\n"
+ "• Lehetővé teszi a manuális órajel és feszültség állítást (LACT-ban)\n"
+ "• Alapértelmezetten az AMD zárolja ezeket a funkciókat\n"
+ "• Beállítás: /etc/modprobe.d/99-amdgpu-overdrive.conf\n"
+ "FIGYELEM: initramfs újraépítést igényel, újraindítás szükséges utána!",
+ "en": "Unlock AMD GPU Overdrive features:\n"
+ "• Enables manual clock and voltage tuning (in LACT)\n"
+ "• AMD locks these features by default\n"
+ "• Config: /etc/modprobe.d/99-amdgpu-overdrive.conf\n"
+ "WARNING: requires initramfs rebuild, reboot needed after!"},
+ "opt_amd_powercap": {"hu": "AMD GPU Power Cap — Maximum",
+ "en": "AMD GPU Power Cap — Maximum"},
+ "opt_amd_powercap_desc": {"hu": "AMD GPU teljesítmény korlát feloldása maximumra:\n"
+ "• A GPU alapból korlátozott TDP-re van beállítva\n"
+ "• Ez beállítja a power1_cap értékét a maximálisan engedélyezett értékre\n"
+ "• Eredmény: GPU teljes teljesítményen futhat, magasabb FPS csúcsok\n"
+ "• Permanensen mentve: /etc/tmpfiles.d/amd-powercap.conf",
+ "en": "Unlock AMD GPU power cap to maximum:\n"
+ "• GPU is limited to restricted TDP by default\n"
+ "• Sets power1_cap to the maximum allowed value\n"
+ "• Result: GPU can run at full power, higher FPS peaks\n"
+ "• Saved permanently: /etc/tmpfiles.d/amd-powercap.conf"},
+ "opt_nvidia_perf": {"hu": "Nvidia Maximum Performance",
+ "en": "Nvidia Maximum Performance"},
+ "opt_nvidia_perf_desc": {"hu": "Nvidia GPU teljesítmény maximalizálása:\n"
+ "• nvidia-smi persistence mode bekapcsolva (gyorsabb driver válasz)\n"
+ "• Auto-boost letiltva régi kártyákon (stabilabb órajel)\n"
+ "• GPU firmware offload letiltva (NVreg_EnableGpuFirmware=0)\n"
+ "Permanensen mentve: /etc/modprobe.d/99-nvidia-gaming.conf\n"
+ "FIGYELEM: initramfs újraépítést igényel, újraindítás szükséges utána!",
+ "en": "Maximize Nvidia GPU performance:\n"
+ "• nvidia-smi persistence mode on (faster driver response)\n"
+ "• Auto-boost disabled on older cards (more stable clocks)\n"
+ "• GPU firmware offload disabled (NVreg_EnableGpuFirmware=0)\n"
+ "Saved permanently: /etc/modprobe.d/99-nvidia-gaming.conf\n"
+ "WARNING: requires initramfs rebuild, reboot needed after!"},
+ "opt_apply": {"hu": "TELEPÍTÉS / ALKALMAZÁS", "en": "INSTALL / APPLY"},
+ "opt_apply_empty": {"hu": "Nincs kijelölve semmi.", "en": "Nothing selected."},
+ "opt_apply_missing":{"hu": "Hiányzó script: ", "en": "Missing script: "},
+ "opt_apply_ok": {"hu": "Sikeresen alkalmazva!", "en": "Successfully applied!"},
+ "opt_apply_err": {"hu": "Hiba (kód: {code}) — napló: {log}",
+ "en": "Error (code: {code}) — log: {log}"},
+ # Hyprland page
+ "hypr_title": {"hu": "HYPRLAND — KEYBINDS", "en": "HYPRLAND — KEYBINDS"},
+ "hypr_source": {"hu": "Forrás: ", "en": "Source: "},
+ "hypr_none": {"hu": "Nem található Hyprland konfiguráció.", "en": "No Hyprland configuration found."},
+ # Hyprland descriptions
+ "hypr_spotlight": {"hu": "Spotlight (DMS) megnyitása", "en": "Open Spotlight (DMS)"},
+ "hypr_terminal": {"hu": "Terminál megnyitása (Kitty)", "en": "Open terminal (Kitty)"},
+ "hypr_filemanager": {"hu": "Fájlkezelő megnyitása (Thunar)", "en": "Open file manager (Thunar)"},
+ "hypr_launcher": {"hu": "App launcher megnyitása", "en": "Open app launcher"},
+ "hypr_logout": {"hu": "Kijelentkezés / leállítás", "en": "Logout / shutdown"},
+ "hypr_exit": {"hu": "Hyprland kilépés", "en": "Exit Hyprland"},
+ "hypr_volume": {"hu": "Hangerő állítás", "en": "Adjust volume"},
+ "hypr_mute": {"hu": "Némítás ki/be", "en": "Toggle mute"},
+ "hypr_brightness": {"hu": "Fényerő állítás", "en": "Adjust brightness"},
+ "hypr_next_track": {"hu": "Következő szám", "en": "Next track"},
+ "hypr_prev_track": {"hu": "Előző szám", "en": "Previous track"},
+ "hypr_play_pause": {"hu": "Lejátszás / szünet", "en": "Play / pause"},
+ "hypr_clipboard": {"hu": "Vágólap megjelenítése", "en": "Show clipboard"},
+ "hypr_screenshot": {"hu": "Képernyőkép készítése", "en": "Take screenshot"},
+ "hypr_lock": {"hu": "Képernyőzár", "en": "Lock screen"},
+ "hypr_kill": {"hu": "Aktív ablak bezárása", "en": "Close active window"},
+ "hypr_float": {"hu": "Lebegő mód ki/be", "en": "Toggle floating mode"},
+ "hypr_pseudo": {"hu": "Pseudo-tiling ki/be", "en": "Toggle pseudo-tiling"},
+ "hypr_fullscreen": {"hu": "Teljes képernyős mód", "en": "Fullscreen mode"},
+ "hypr_exit2": {"hu": "Kilépés", "en": "Exit"},
+ "hypr_scratchpad": {"hu": "Scratchpad megjelenítése", "en": "Toggle scratchpad"},
+ "hypr_focus_l": {"hu": "Fókusz mozgatása balra", "en": "Focus left"},
+ "hypr_focus_r": {"hu": "Fókusz mozgatása jobbra", "en": "Focus right"},
+ "hypr_focus_u": {"hu": "Fókusz mozgatása fel", "en": "Focus up"},
+ "hypr_focus_d": {"hu": "Fókusz mozgatása le", "en": "Focus down"},
+ "hypr_ws_goto": {"hu": "Váltás {p}. munkaterületre", "en": "Switch to workspace {p}"},
+ "hypr_ws_move": {"hu": "Ablak áthelyezése {p}. munkaterületre", "en": "Move window to workspace {p}"},
+ "hypr_ws_generic": {"hu": "Munkaterület: {p}", "en": "Workspace: {p}"},
+ "hypr_ws_scratch": {"hu": "Ablak → scratchpad", "en": "Window → scratchpad"},
+ "hypr_ws_scratch_t":{"hu": "Scratchpad ki/be", "en": "Toggle scratchpad"},
+ "hypr_ws_next": {"hu": "Következő munkaterület", "en": "Next workspace"},
+ "hypr_ws_prev": {"hu": "Előző munkaterület", "en": "Previous workspace"},
+ "hypr_ws_goto_n": {"hu": "Váltás {n}. munkaterületre", "en": "Switch to workspace {n}"},
+ "hypr_ws_move_n": {"hu": "Ablak áthelyezése {n}. munkaterületre", "en": "Move window to workspace {n}"},
+ "hypr_layout": {"hu": "Layout: {p}", "en": "Layout: {p}"},
+ "hypr_run": {"hu": "Futtatás: {prog}", "en": "Run: {prog}"},
+ "hypr_run_generic": {"hu": "Program futtatása", "en": "Run program"},
+ # Credits page
+ "credits_title": {"hu": "KÖZREMŰKÖDŐK", "en": "CONTRIBUTORS"},
+ "credits_rp1_name": {"hu": "RavePriest1", "en": "RavePriest1"},
+ "credits_rp1_role": {"hu": "Névadó, hivatalos naplopó", "en": "Namesake, official slacker"},
+ "credits_rp1_desc": {"hu": "Nem tudja mit akar — magyar streamer.", "en": "Doesn't know what he wants — Hungarian streamer."},
+ "credits_alexc_name": {"hu": "AlexC", "en": "AlexC"},
+ "credits_alexc_role": {"hu": "Arch alapkő", "en": "Arch cornerstone"},
+ "credits_alexc_desc": {"hu": "Hallóóó mester.", "en": "Hellooo master."},
+ "credits_nippy_name": {"hu": "Nippy", "en": "Nippy"},
+ "credits_nippy_role": {"hu": "Fejlesztő", "en": "Developer"},
+ "credits_nippy_desc": {"hu": "Agyérgörcsöt kap PR1-től, mert nem tudja mit akar.", "en": "Gets an aneurysm from PR1 because he doesn't know what he wants."},
+ "credits_gabesz_name": {"hu": "GabeszM", "en": "GabeszM"},
+ "credits_gabesz_role": {"hu": "Fejlesztő", "en": "Developer"},
+ "credits_gabesz_desc": {"hu": "Weboldal, GNOME kiegészítő és alt-tab dizájn — mert valakinek kellett.", "en": "Website, GNOME extension and alt-tab design — because someone had to."},
+ "credits_stefi_name": {"hu": "Stefi", "en": "Stefi"},
+ "credits_stefi_role": {"hu": "Design", "en": "Design"},
+ "credits_stefi_desc": {"hu": "5000 lesz!", "en": "That'll be 5000!"},
+ # Finish page
+ "finish_ok": {"hu": "OK", "en": "OK"},
+ "finish_title": {"hu": "KÉSZ!", "en": "DONE!"},
+ "finish_sub": {"hu": "A RaveOS be van állítva.", "en": "RaveOS is set up."},
+ "finish_next_step": {"hu": "KÖVETKEZŐ LÉPÉS", "en": "NEXT STEP"},
+ "finish_inst_desc": {"hu": "Az App Installer segítségével telepíthetsz minden fontos "
+ "szoftvert egy helyen — böngészőtől kezdve a fejlesztőeszközökig.",
+ "en": "With the App Installer you can install all important "
+ "software in one place — from browsers to developer tools."},
+ "finish_install": {"hu": "APP INSTALLER INDÍTÁSA", "en": "LAUNCH APP INSTALLER"},
+ "finish_close": {"hu": "Bezárás", "en": "Close"},
+ "finish_install_missing": {"hu": "Nem található az App Installer.", "en": "App Installer not found."},
+ "finish_install_err": {"hu": "Nem sikerült elindítani. Napló: {log}", "en": "Failed to launch. Log: {log}"},
+ # Tooltip keys (for optimize page descriptions that use _ACTION_DESCS)
+ "action_killactive": {"hu": "Aktív ablak bezárása", "en": "Close active window"},
+ "action_togglefloat": {"hu": "Lebegő mód ki/be", "en": "Toggle floating mode"},
+ "action_pseudo": {"hu": "Pseudo-tiling ki/be", "en": "Toggle pseudo-tiling"},
+ "action_fullscreen": {"hu": "Teljes képernyős mód", "en": "Fullscreen mode"},
+ "action_exit": {"hu": "Kilépés", "en": "Exit"},
+ "action_scratchpad": {"hu": "Scratchpad megjelenítése", "en": "Toggle scratchpad"},
+ "action_focus_l": {"hu": "Fókusz mozgatása balra", "en": "Focus left"},
+ "action_focus_r": {"hu": "Fókusz mozgatása jobbra", "en": "Focus right"},
+ "action_focus_u": {"hu": "Fókusz mozgatása fel", "en": "Focus up"},
+ "action_focus_d": {"hu": "Fókusz mozgatása le", "en": "Focus down"},
+}
+
+
+def _t(key, **kwargs):
+ """Get translation for key, with optional format kwargs."""
+ lang = _current_lang
+ text = T.get(key, {}).get(lang, T.get(key, {}).get("hu", key))
+ if kwargs:
+ try:
+ return text.format(**kwargs)
+ except (KeyError, IndexError):
+ return text
+ return text
+
+
+STYLESHEET = f"""
+QMainWindow, QWidget#central {{
+ background: {COLORS['bg']};
+}}
+
+QWidget {{
+ color: {COLORS['text']};
+ font-family: 'Ubuntu', sans-serif;
+}}
+
+QLabel#title {{
+ font-size: 38px;
+ font-weight: 700;
+ color: {COLORS['text']};
+}}
+
+QLabel#subtitle {{
+ font-size: 14px;
+ color: {COLORS['text_dim']};
+ letter-spacing: 1px;
+}}
+
+QLabel#section_title {{
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 2px;
+ color: {COLORS['accent_light']};
+ text-transform: uppercase;
+}}
+
+QLabel#body {{
+ font-size: 14px;
+ color: {COLORS['text_dim']};
+ line-height: 1.6;
+}}
+
+QPushButton#primary {{
+ background: {COLORS['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 12px 32px;
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 1px;
+ min-width: 140px;
+}}
+QPushButton#primary:hover {{
+ background: {COLORS['accent_hover']};
+}}
+QPushButton#primary:pressed {{
+ background: {COLORS['accent_hover']};
+ padding: 13px 31px 11px 33px;
+}}
+QPushButton#primary:disabled {{
+ background: {COLORS['surface2']};
+ color: {COLORS['text_dim']};
+}}
+
+QPushButton#secondary {{
+ background: transparent;
+ color: {COLORS['text_dim']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 6px;
+ padding: 12px 32px;
+ font-size: 13px;
+ font-weight: 500;
+ min-width: 100px;
+}}
+QPushButton#secondary:hover {{
+ border-color: {COLORS['accent']};
+ color: {COLORS['accent_light']};
+}}
+
+QPushButton#icon_btn {{
+ background: {COLORS['surface']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 6px;
+ padding: 8px 14px;
+ font-size: 13px;
+ color: {COLORS['text_dim']};
+}}
+QPushButton#icon_btn:hover {{
+ border-color: {COLORS['accent']};
+ color: {COLORS['accent_light']};
+ background: {COLORS['surface2']};
+}}
+
+QListWidget {{
+ background: {COLORS['surface']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 8px;
+ padding: 4px;
+ outline: none;
+}}
+QListWidget::item {{
+ padding: 10px 14px;
+ border-radius: 4px;
+ font-size: 14px;
+ color: {COLORS['text']};
+ border: none;
+}}
+QListWidget::item:hover {{
+ background: {COLORS['surface2']};
+}}
+QListWidget::item:selected {{
+ background: rgba(61, 120, 57, 0.20);
+ color: {COLORS['accent_light']};
+ border: 1px solid rgba(61, 120, 57, 0.40);
+}}
+
+QLineEdit {{
+ background: {COLORS['surface']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 6px;
+ padding: 10px 14px;
+ font-size: 14px;
+ color: {COLORS['text']};
+}}
+QLineEdit:focus {{
+ border-color: {COLORS['accent']};
+}}
+QLineEdit::placeholder {{
+ color: {COLORS['text_dim']};
+}}
+
+QProgressBar {{
+ background: {COLORS['surface']};
+ border: none;
+ border-radius: 2px;
+ height: 4px;
+ text-align: center;
+ font-size: 0px;
+}}
+QProgressBar::chunk {{
+ background: {COLORS['accent']};
+ border-radius: 2px;
+}}
+
+QFrame#separator {{
+ background: {COLORS['accent']};
+ max-height: 1px;
+ min-height: 1px;
+ opacity: 0.4;
+}}
+
+QFrame#card {{
+ background: {COLORS['surface']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 8px;
+}}
+
+QToolTip {{
+ background: {COLORS['bg2']};
+ color: {COLORS['text']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 6px 10px;
+ font-size: 12px;
+}}
+"""
+
+# ─── WiFi Worker ───────────────────────────────────────────────────────────────
+
+class WiFiScanner(QThread):
+ result = pyqtSignal(list)
+ error = pyqtSignal(str)
+
+ def run(self):
+ try:
+ out = subprocess.check_output(
+ ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY", "dev", "wifi", "list", "--rescan", "yes"],
+ stderr=subprocess.DEVNULL, timeout=10
+ ).decode().strip()
+ networks = []
+ seen = set()
+ for line in out.splitlines():
+ parts = line.split(":")
+ if len(parts) >= 2:
+ ssid = parts[0].strip()
+ if not ssid or ssid in seen:
+ continue
+ seen.add(ssid)
+ try:
+ signal = int(parts[1])
+ except ValueError:
+ signal = 0
+ security = parts[2].strip() if len(parts) > 2 else ""
+ networks.append({"ssid": ssid, "signal": signal, "security": security})
+ networks.sort(key=lambda x: x["signal"], reverse=True)
+ self.result.emit(networks)
+ except Exception as e:
+ self.error.emit(str(e))
+
+
+class WiFiConnector(QThread):
+ success = pyqtSignal()
+ failure = pyqtSignal(str)
+
+ def __init__(self, ssid, password=""):
+ super().__init__()
+ self.ssid = ssid
+ self.password = password
+
+ def run(self):
+ try:
+ if self.password:
+ cmd = ["nmcli", "dev", "wifi", "connect", self.ssid,
+ "password", self.password]
+ else:
+ cmd = ["nmcli", "dev", "wifi", "connect", self.ssid]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
+ if result.returncode == 0:
+ self.success.emit()
+ else:
+ self.failure.emit(result.stderr.strip() or _t("wifi_unknown_err"))
+ except subprocess.TimeoutExpired:
+ self.failure.emit(_t("wifi_timeout"))
+ except Exception as e:
+ self.failure.emit(str(e))
+
+
+# ─── Logo Widget ───────────────────────────────────────────────────────────────
+
+def _find_svg() -> str:
+ if Path(SVG_LOGO).exists():
+ return SVG_LOGO
+ candidate = _SCRIPT_DIR / "raveos-welcome.svg"
+ if candidate.exists():
+ return str(candidate)
+ return ""
+
+
+class RaveLogo(QWidget):
+ """RaveOS SVG logo, nagy meretben."""
+
+ SIZE = 180
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setFixedSize(self.SIZE, self.SIZE)
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ svg_path = _find_svg()
+ if svg_path:
+ self._svg = QSvgWidget(svg_path, self)
+ self._svg.setFixedSize(self.SIZE, self.SIZE)
+ layout.addWidget(self._svg)
+ else:
+ fallback = QLabel("RAVE OS", self)
+ fallback.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ fallback.setStyleSheet(
+ f"font-size: 32px; font-weight: 700; color: {COLORS['text']};"
+ f"font-family: Ubuntu;"
+ )
+ layout.addWidget(fallback)
+
+
+# ─── WiFi page visibility check ───────────────────────────────────────────────
+
+def _should_show_wifi() -> bool:
+ try:
+ out = subprocess.check_output(
+ ["nmcli", "-t", "-f", "TYPE,STATE", "dev"],
+ stderr=subprocess.DEVNULL, timeout=1
+ ).decode()
+ except Exception:
+ return False
+
+ has_wifi = False
+ for line in out.splitlines():
+ parts = line.split(":")
+ if len(parts) < 2:
+ continue
+ dev_type, state = parts[0], parts[1]
+ if dev_type == "wifi":
+ has_wifi = True
+ if dev_type == "ethernet" and state == "connected":
+ return False # aktív vezetékes kapcsolat van, wifi oldal nem kell
+
+ return has_wifi
+
+
+# ─── Signal bar helper ─────────────────────────────────────────────────────────
+
+def signal_icon(signal: int) -> str:
+ if signal >= 80: return "▂▄▆█"
+ if signal >= 60: return "▂▄▆·"
+ if signal >= 40: return "▂▄··"
+ return "▂···"
+
+
+def security_tag(sec: str) -> str:
+ if not sec or sec == "--":
+ return _t("wifi_open")
+ return _t("wifi_encrypted")
+
+
+# ─── Page 1: Welcome ───────────────────────────────────────────────────────────
+
+class WelcomePage(QWidget):
+ def __init__(self):
+ super().__init__()
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 40, 60, 40)
+ lay.setSpacing(0)
+
+ # Logo
+ logo = RaveLogo()
+ lay.addWidget(logo, alignment=Qt.AlignmentFlag.AlignHCenter)
+ lay.addSpacing(10)
+
+ title_lbl = QLabel()
+ title_lbl.setTextFormat(Qt.TextFormat.RichText)
+ title_lbl.setText(
+ f'<span style="font-size:38px;font-weight:700;color:{COLORS["text"]};">RAVE</span>'
+ f'<span style="font-size:48px;font-weight:700;color:{COLORS["accent_light"]};">OS</span>'
+ )
+ title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(title_lbl)
+ lay.addSpacing(2)
+
+ self.tag = QLabel(_t("welcome_tag"))
+ self.tag.setObjectName("subtitle")
+ self.tag.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.tag)
+
+ sep = QFrame()
+ sep.setObjectName("separator")
+ sep.setFixedHeight(1)
+ lay.addSpacing(28)
+ lay.addWidget(sep)
+ lay.addSpacing(24)
+
+ # About card
+ card = QFrame()
+ card.setObjectName("card")
+ card_lay = QVBoxLayout(card)
+ card_lay.setContentsMargins(28, 20, 28, 20)
+ card_lay.setSpacing(14)
+
+ self.about = QLabel(_t("welcome_about"))
+ self.about.setObjectName("body")
+ self.about.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.about.setWordWrap(True)
+ card_lay.addWidget(self.about)
+
+ sep2 = QFrame()
+ sep2.setObjectName("separator")
+ sep2.setFixedHeight(1)
+ card_lay.addWidget(sep2)
+
+ features_lay = QHBoxLayout()
+ features_lay.setSpacing(24)
+ self._feature_icons = []
+ self._feature_labels = []
+ for icon, key in [
+ ("⚡︎", "feat_perf"),
+ ("🎮︎", "feat_gaming"),
+ ("🛠︎", "feat_apps"),
+ ]:
+ col = QVBoxLayout()
+ col.setSpacing(4)
+ ico = QLabel(icon)
+ ico.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ ico.setStyleSheet(f"font-size: 22px; color: {COLORS['text']};")
+ lbl = QLabel(_t(key))
+ lbl.setObjectName("body")
+ lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lbl.setStyleSheet(f"font-size: 11px; color: {COLORS['text_dim']};")
+ col.addWidget(ico)
+ col.addWidget(lbl)
+ features_lay.addLayout(col)
+ self._feature_icons.append(icon)
+ self._feature_labels.append((key, lbl))
+ card_lay.addLayout(features_lay)
+
+ lay.addWidget(card)
+ lay.addSpacing(18)
+
+ # Community links
+ links_row1 = QHBoxLayout()
+ links_row1.setSpacing(8)
+ links_row2 = QHBoxLayout()
+ links_row2.setSpacing(8)
+
+ all_links = [
+ ("support", "https://ko-fi.com/ravepriest1/tiers"),
+ ("RP Forgejo", "https://git.rp1.hu/explore/repos"),
+ ("RP YouTube", "https://www.youtube.com/@RPslair"),
+ ("RP Twitch", "https://www.twitch.tv/ravepriest1"),
+ ("RP Kick", "https://kick.com/rpslair"),
+ ("RP Discord", "https://discord.gg/gSdVMXRFQc"),
+ ]
+ self._link_btns = []
+ self._support_btn = None
+ for i, (label, url) in enumerate(all_links):
+ if label == "support":
+ btn = QPushButton(_t("btn_support"))
+ self._support_btn = btn
+ else:
+ btn = QPushButton(label)
+ btn.setObjectName("icon_btn")
+ btn.clicked.connect(lambda _, u=url: self._open_url(u))
+ if label == "support":
+ btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['surface']}; border: 1px solid {COLORS['accent']};"
+ f" border-radius: 6px; padding: 8px 14px; font-size: 13px;"
+ f" color: #ffffff; font-weight: 700; }}"
+ f"QPushButton:hover {{ border-color: {COLORS['accent_light']};"
+ f" color: {COLORS['accent_light']}; background: {COLORS['surface2']}; }}"
+ )
+ if i < 3:
+ links_row1.addWidget(btn)
+ else:
+ links_row2.addWidget(btn)
+ self._link_btns.append((label, btn))
+
+ lay.addLayout(links_row1)
+ lay.addSpacing(6)
+ lay.addLayout(links_row2)
+ lay.addStretch()
+
+ def retranslate(self):
+ self.tag.setText(_t("welcome_tag"))
+ self.about.setText(_t("welcome_about"))
+ for key, lbl in self._feature_labels:
+ lbl.setText(_t(key))
+ if self._support_btn:
+ self._support_btn.setText(_t("btn_support"))
+
+ def _open_url(self, url):
+ subprocess.Popen(["xdg-open", url])
+
+
+# ─── Page 2: WiFi ──────────────────────────────────────────────────────────────
+
+class WiFiPage(QWidget):
+ def __init__(self):
+ super().__init__()
+ self._networks = []
+ self._selected_ssid = None
+ self._scanner = None
+ self._connector = None
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 40, 60, 40)
+ lay.setSpacing(16)
+
+ self.title = QLabel(_t("wifi_title"))
+ self.title.setObjectName("section_title")
+ lay.addWidget(self.title)
+
+ h = QHBoxLayout()
+ h.setSpacing(10)
+ self.net_title = QLabel(_t("wifi_subtitle"))
+ self.net_title.setObjectName("subtitle")
+ h.addWidget(self.net_title)
+ h.addStretch()
+
+ self.scan_btn = QPushButton(_t("wifi_scan"))
+ self.scan_btn.setObjectName("icon_btn")
+ self.scan_btn.clicked.connect(self.scan)
+ h.addWidget(self.scan_btn)
+ lay.addLayout(h)
+
+ self.progress = QProgressBar()
+ self.progress.setRange(0, 0)
+ self.progress.setFixedHeight(4)
+ self.progress.setVisible(False)
+ lay.addWidget(self.progress)
+
+ self.list = QListWidget()
+ self.list.setMinimumHeight(180)
+ self.list.itemClicked.connect(self._on_select)
+ lay.addWidget(self.list)
+
+ # Password field
+ self.pw_frame = QFrame()
+ pw_lay = QVBoxLayout(self.pw_frame)
+ pw_lay.setContentsMargins(0, 0, 0, 0)
+ pw_lay.setSpacing(6)
+
+ self.pw_label = QLabel(_t("wifi_password"))
+ self.pw_label.setObjectName("body")
+ pw_lay.addWidget(self.pw_label)
+
+ self.pw_input = QLineEdit()
+ self.pw_input.setPlaceholderText(_t("wifi_password"))
+ self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)
+ pw_lay.addWidget(self.pw_input)
+
+ self.pw_frame.setVisible(False)
+ lay.addWidget(self.pw_frame)
+
+ # Status
+ self.status_label = QLabel("")
+ self.status_label.setObjectName("body")
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.status_label)
+
+ # Connect button
+ self.connect_btn = QPushButton(_t("wifi_connect"))
+ self.connect_btn.setObjectName("primary")
+ self.connect_btn.setEnabled(False)
+ self.connect_btn.clicked.connect(self._connect)
+ lay.addWidget(self.connect_btn, alignment=Qt.AlignmentFlag.AlignRight)
+
+ lay.addStretch()
+
+ def retranslate(self):
+ self.title.setText(_t("wifi_title"))
+ self.net_title.setText(_t("wifi_subtitle"))
+ self.scan_btn.setText(_t("wifi_scan"))
+ self.connect_btn.setText(_t("wifi_connect"))
+ if self._selected_ssid:
+ self.status_label.setText(_t("wifi_selected", ssid=self._selected_ssid))
+
+ def showEvent(self, event):
+ super().showEvent(event)
+ if not self._networks:
+ self.scan()
+
+ def scan(self):
+ self.scan_btn.setEnabled(False)
+ self.list.clear()
+ self.status_label.setText(_t("wifi_scanning"))
+ self.progress.setVisible(True)
+ self._scanner = WiFiScanner()
+ self._scanner.result.connect(self._on_scan_result)
+ self._scanner.error.connect(self._on_scan_error)
+ self._scanner.start()
+
+ def _on_scan_result(self, networks):
+ self.progress.setVisible(False)
+ self.scan_btn.setEnabled(True)
+ self._networks = networks
+ self.list.clear()
+ if not networks:
+ self.status_label.setText(_t("wifi_none"))
+ return
+ self.status_label.setText(_t("wifi_found", n=len(networks)))
+ for n in networks:
+ text = f"{signal_icon(n['signal'])} {n['ssid']}{security_tag(n['security'])}"
+ item = QListWidgetItem(text)
+ item.setData(Qt.ItemDataRole.UserRole, n)
+ self.list.addItem(item)
+
+ def _on_scan_error(self, err):
+ self.progress.setVisible(False)
+ self.scan_btn.setEnabled(True)
+ self.status_label.setStyleSheet(f"color: {COLORS['error']};")
+ self.status_label.setText(_t("wifi_error", err=err))
+
+ def _on_select(self, item):
+ n = item.data(Qt.ItemDataRole.UserRole)
+ self._selected_ssid = n["ssid"]
+ needs_pw = bool(n["security"] and n["security"] != "--")
+ self.pw_frame.setVisible(needs_pw)
+ if needs_pw:
+ self.pw_label.setText(f"{_t('wifi_password')} ({n['ssid']}):")
+ self.connect_btn.setEnabled(True)
+ self.status_label.setStyleSheet("")
+ self.status_label.setText(_t("wifi_selected", ssid=n['ssid']))
+
+ def _connect(self):
+ if not self._selected_ssid:
+ return
+ pw = self.pw_input.text() if self.pw_frame.isVisible() else ""
+ self.connect_btn.setEnabled(False)
+ self.progress.setVisible(True)
+ self.status_label.setStyleSheet(f"color: {COLORS['text_dim']};")
+ self.status_label.setText(_t("wifi_connecting"))
+ self._connector = WiFiConnector(self._selected_ssid, pw)
+ self._connector.success.connect(self._on_connected)
+ self._connector.failure.connect(self._on_connect_fail)
+ self._connector.start()
+
+ def _on_connected(self):
+ self.progress.setVisible(False)
+ self.status_label.setStyleSheet(f"color: {COLORS['success']};")
+ self.status_label.setText(_t("wifi_connected", ssid=self._selected_ssid))
+ self.connect_btn.setEnabled(True)
+
+ def _on_connect_fail(self, err):
+ self.progress.setVisible(False)
+ self.status_label.setStyleSheet(f"color: {COLORS['error']};")
+ self.status_label.setText(_t("wifi_error", err=err))
+ self.connect_btn.setEnabled(True)
+
+
+# ─── Hyprland Detection ───────────────────────────────────────────────────────
+
+HYPR_CONFIG = Path.home() / ".config" / "hypr" / "hyprland.conf"
+_HYPR_FALLBACK = Path("/usr/share/raveos/hyprland-theme/theme-data/hypr/hyprland.conf")
+
+# Hyprland 0.55+ Lua config mod (RaveOS ezt hasznalja alapertelmezetten,
+# nincs is hagyomanyos hyprland.conf a rendszeren)
+HYPR_KEYBINDS_LUA = Path.home() / ".config" / "hypr" / "config" / "keybinds.lua"
+_HYPR_KEYBINDS_LUA_FALLBACK = Path(
+ "/usr/share/raveos/hyprland-theme/theme-data/hypr/config/keybinds.lua"
+)
+
+def _detect_hyprland() -> bool:
+ if os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == "hyprland":
+ return True
+ if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
+ return True
+ return False
+
+
+def _fmt_key(raw: str) -> str:
+ mapping = {
+ "super": "Win", "ctrl": "Ctrl", "alt": "Alt", "shift": "Shift",
+ "return": "Enter", "space": "Space", "tab": "Tab",
+ "left": "←", "right": "→", "up": "↑", "down": "↓",
+ "mouse_down": "Görgő le", "mouse_up": "Görgő fel",
+ "mouse:272": "Bal egér", "mouse:273": "Jobb egér",
+ "super_l": "Win",
+ }
+ parts = [p.strip() for p in raw.replace("+", " ").split() if p.strip()]
+ result = []
+ for p in parts:
+ result.append(mapping.get(p.lower(), p.upper()))
+ return " + ".join(result)
+
+
+_EXEC_SNIPPETS = {
+ "dms ipc call spotlight toggle": "Spotlight (DMS) megnyitása",
+ "dms ipc call clipboard toggle": "Vágólap megjelenítése",
+ "dms ipc call processlist toggle": "Folyamatlista megjelenítése",
+ "dms ipc call notifications toggle": "Értesítések megjelenítése",
+ "dms ipc call settings toggle": "Beállítások megnyitása",
+ "dms ipc call dankdash wallpaper": "Háttérkép váltó megnyitása",
+ "hyprshell socat": "Ablakváltó (Alt+Tab) megnyitása",
+ "reboot": "Újraindítás",
+ "poweroff": "Leállítás",
+ "freetube": "FreeTube megnyitása",
+ "discord": "Discord megnyitása",
+ "gedit": "Szövegszerkesztő megnyitása",
+ "pavucontrol": "Hangkezelő megnyitása",
+ "kitty": "Terminál megnyitása (Kitty)",
+ "thunar": "Fájlkezelő megnyitása (Thunar)",
+ "hyperlauncher": "App launcher megnyitása",
+ "hyprlauncher": "App launcher megnyitása",
+ "hyprshutdown": "Kijelentkezés / leállítás",
+ "hyprctl dispatch exit": "Hyprland kilépés",
+ "wpctl set-volume": "Hangerő állítás",
+ "wpctl set-mute": "Némítás ki/be",
+ "brightnessctl": "Fényerő állítás",
+ "playerctl next": "Következő szám",
+ "playerctl previous": "Előző szám",
+ "playerctl play-pause": "Lejátszás / szünet",
+ "rofi": "App launcher (Rofi)",
+ "wofi": "App launcher (Wofi)",
+ "wl-paste": "Vágólap kezelő",
+ "cliphist": "Vágólap megjelenítése",
+ "grim": "Képernyőkép készítése",
+ "grimblast": "Képernyőkép készítése",
+ "hyprlock": "Képernyőzár",
+ "swaylock": "Képernyőzár",
+}
+
+_ACTION_DESCS = {
+ "killactive": "Aktív ablak bezárása",
+ "togglefloating": "Lebegő mód ki/be",
+ "pseudo": "Pseudo-tiling ki/be",
+ "fullscreen": "Teljes képernyős mód",
+ "exit": "Kilépés",
+ "movefocus": {
+ "l": "Fókusz mozgatása balra",
+ "r": "Fókusz mozgatása jobbra",
+ "u": "Fókusz mozgatása fel",
+ "d": "Fókusz mozgatása le",
+ },
+ "togglespecialworkspace": "Scratchpad megjelenítése",
+ "workspace": lambda p: (
+ f"Váltás {p}. munkaterületre" if p.isdigit() else
+ f"Munkaterület: {p}"
+ ),
+ "movetoworkspace": lambda p: (
+ f"Ablak áthelyezése {p}. munkaterületre" if p.isdigit() else
+ f"Ablak áthelyezése: {p}"
+ ),
+ "layoutmsg": lambda p: f"Layout: {p}",
+}
+
+
+def _exec_desc(cmd: str) -> str:
+ cmd_lower = cmd.lower()
+ for snippet, label in _EXEC_SNIPPETS.items():
+ if snippet.lower() in cmd_lower:
+ return label
+ prog = cmd.strip().split()[0].split("/")[-1] if cmd.strip() else ""
+ return f"Futtatás: {prog}" if prog else "Program futtatása"
+
+
+def _workspace_desc(prefix: str, params: str) -> str:
+ p = params.strip()
+ if p.startswith("special:"):
+ return "Ablak → scratchpad" if prefix == "move" else "Scratchpad ki/be"
+ if p in ("e+1",):
+ return "Következő munkaterület"
+ if p in ("e-1",):
+ return "Előző munkaterület"
+ if p.isdigit():
+ n = p if p != "0" else "10"
+ return f"Ablak → {n}. munkaterületre" if prefix == "move" else f"Váltás {n}. munkaterületre"
+ return f"Munkaterület: {p}"
+
+
+def _action_desc(action: str, params: str) -> str:
+ if action in ("workspace",):
+ return _workspace_desc("", params)
+ if action in ("movetoworkspace",):
+ return _workspace_desc("move", params)
+ entry = _ACTION_DESCS.get(action)
+ if entry is None:
+ if action == "exec":
+ return _exec_desc(params)
+ return f"{action} {params}".strip()
+ if callable(entry):
+ return entry(params)
+ if isinstance(entry, dict):
+ return entry.get(params.strip(), f"Fókusz mozgatása ({params})")
+ return entry
+
+
+def _parse_hypr_binds(config_path: Path) -> list:
+ if not config_path.exists():
+ return []
+ try:
+ text = config_path.read_text(errors="replace")
+ except Exception:
+ return []
+
+ vars_ = {}
+ binds = []
+
+ for line in text.splitlines():
+ line = line.strip()
+ if not line or line.startswith("#"):
+ continue
+
+ if line.startswith("$") and "=" in line:
+ k, _, v = line.partition("=")
+ vars_[k.strip()] = v.split("#")[0].strip()
+ continue
+
+ if not line.startswith("bind"):
+ continue
+ if line.startswith("bindm"):
+ continue
+
+ eq = line.find("=")
+ if eq < 0:
+ continue
+ rest = line[eq + 1:].strip()
+
+ for vn, vv in vars_.items():
+ rest = rest.replace(vn, vv)
+
+ parts = [p.strip() for p in rest.split(",")]
+ if len(parts) < 3:
+ continue
+
+ mods_raw = parts[0]
+ key_raw = parts[1]
+ action = parts[2].lower().strip()
+ if action == "exec" and len(parts) > 3:
+ params = ",".join(parts[3:]).split("#")[0].strip()
+ elif len(parts) > 3:
+ params = parts[3].split("#")[0].strip()
+ else:
+ params = ""
+
+ if "XF86" in key_raw:
+ continue
+
+ combo = _fmt_key(f"{mods_raw} {key_raw}" if mods_raw else key_raw)
+ desc = _action_desc(action, params)
+ if combo and desc:
+ binds.append((combo, desc))
+
+ return binds
+
+
+# ─── Hyprland Lua config parser (keybinds.lua, hl.bind(...) hivasok) ─────────
+# Best-effort parser: nem valodi Lua interpreter, csak a RaveOS keybinds.lua
+# tipikus mintait (hl.bind("KEY", hl.dsp.xxx(...)), local string valtozok,
+# egyszeru "for i = a, b do ... end" munkaterulet-ciklusok) ismeri fel.
+
+def _lua_extract_call(line: str, funcname: str):
+ marker = funcname + "("
+ idx = line.find(marker)
+ if idx < 0:
+ return None
+ start = idx + len(marker)
+ depth = 1
+ i = start
+ in_str = False
+ str_char = ""
+ while i < len(line) and depth > 0:
+ c = line[i]
+ if in_str:
+ if c == str_char:
+ in_str = False
+ elif c in "\"'":
+ in_str = True
+ str_char = c
+ elif c in "([{":
+ depth += 1
+ elif c in ")]}":
+ depth -= 1
+ i += 1
+ if depth != 0:
+ return None
+ return line[start:i - 1]
+
+
+def _lua_split_args(s: str) -> list:
+ parts = []
+ depth = 0
+ current = []
+ in_str = False
+ str_char = ""
+ for c in s:
+ if in_str:
+ current.append(c)
+ if c == str_char:
+ in_str = False
+ continue
+ if c in "\"'":
+ in_str = True
+ str_char = c
+ current.append(c)
+ continue
+ if c in "([{":
+ depth += 1
+ current.append(c)
+ continue
+ if c in ")]}":
+ depth -= 1
+ current.append(c)
+ continue
+ if c == "," and depth == 0:
+ parts.append("".join(current))
+ current = []
+ continue
+ current.append(c)
+ if current:
+ parts.append("".join(current))
+ return [p.strip() for p in parts]
+
+
+def _lua_resolve_key_expr(expr: str, env: dict):
+ parts = expr.split("..")
+ resolved = []
+ for part in parts:
+ part = part.strip()
+ if len(part) >= 2 and part[0] == part[-1] and part[0] in "\"'":
+ resolved.append(part[1:-1])
+ elif part in env:
+ resolved.append(str(env[part]))
+ else:
+ return None
+ return "".join(resolved)
+
+
+def _lua_unquote(s: str) -> str:
+ s = s.strip()
+ if s.startswith("[[") and s.endswith("]]"):
+ return s[2:-2]
+ if len(s) >= 2 and s[0] == s[-1] and s[0] in "\"'":
+ return s[1:-1]
+ return s
+
+
+def _lua_dispatch_desc(call_expr: str):
+ m = re.match(r'^hl\.dsp\.([\w.]+)\((.*)\)$', call_expr.strip(), re.DOTALL)
+ if not m:
+ return None
+ dispatcher, arg_str = m.group(1), m.group(2)
+ if dispatcher == "exec_cmd":
+ return _exec_desc(_lua_unquote(arg_str))
+ if dispatcher == "window.kill":
+ return "Aktív ablak bezárása"
+ if dispatcher == "window.float":
+ return "Lebegő mód ki/be"
+ if dispatcher == "window.drag":
+ return "Ablak húzása egérrel"
+ if dispatcher == "window.resize":
+ return "Ablak átméretezése egérrel"
+ if dispatcher == "focus":
+ dm = re.search(r'direction\s*=\s*"(\w+)"', arg_str)
+ if dm:
+ names = {"left": "balra", "right": "jobbra", "up": "fel", "down": "le"}
+ return f"Fókusz mozgatása {names.get(dm.group(1), dm.group(1))}"
+ wm = re.search(r'workspace\s*=\s*(-?\d+)', arg_str)
+ if wm:
+ return f"Váltás {wm.group(1)}. munkaterületre"
+ return "Fókusz mozgatása"
+ if dispatcher == "window.move":
+ wm = re.search(r'workspace\s*=\s*(-?\d+)', arg_str)
+ if wm:
+ return f"Ablak áthelyezése {wm.group(1)}. munkaterületre"
+ return "Ablak áthelyezése"
+ return None
+
+
+def _lua_simple_arith(expr: str, env: dict):
+ expr = expr.strip()
+ m = re.match(r'^(\w+)\s*%\s*(\d+)$', expr)
+ if m and m.group(1) in env:
+ try:
+ return int(env[m.group(1)]) % int(m.group(2))
+ except (TypeError, ValueError):
+ return None
+ m = re.match(r'^(\w+)\s*([+-])\s*(\d+)$', expr)
+ if m and m.group(1) in env:
+ try:
+ base = int(env[m.group(1)])
+ return base + int(m.group(3)) if m.group(2) == "+" else base - int(m.group(3))
+ except (TypeError, ValueError):
+ return None
+ if expr.lstrip("-").isdigit():
+ return int(expr)
+ return env.get(expr)
+
+
+def _lua_try_add_bind(line: str, env: dict, binds: list):
+ inner = _lua_extract_call(line, "hl.bind")
+ if inner is None:
+ return
+ args = _lua_split_args(inner)
+ if len(args) < 2:
+ return
+ if "XF86" in args[0]:
+ return
+ combo_raw = _lua_resolve_key_expr(args[0], env)
+ if combo_raw is None:
+ return
+ combo = _fmt_key(combo_raw)
+
+ # ciklus-valtozok (pl. workspace = i) behelyettesitese a dispatcher
+ # kifejezesbe is, hogy a leiras a tenyleges munkaterulet-szamot mutassa
+ dispatch_expr = args[1]
+ for name, value in env.items():
+ if isinstance(value, int):
+ dispatch_expr = re.sub(rf'\b{re.escape(name)}\b', str(value), dispatch_expr)
+
+ desc = _lua_dispatch_desc(dispatch_expr)
+ if combo and desc:
+ binds.append((combo, desc))
+
+
+def _parse_hypr_binds_lua(path: Path) -> list:
+ if not path.exists():
+ return []
+ try:
+ lines = path.read_text(errors="replace").splitlines()
+ except Exception:
+ return []
+
+ env = {}
+ binds = []
+ i = 0
+ n = len(lines)
+ while i < n:
+ line = lines[i].strip()
+ if not line or line.startswith("--"):
+ i += 1
+ continue
+
+ m = re.match(r'^local\s+(\w+)\s*=\s*"([^"]*)"\s*$', line)
+ if m:
+ env[m.group(1)] = m.group(2)
+ i += 1
+ continue
+
+ m = re.match(r'^for\s+(\w+)\s*=\s*(-?\d+)\s*,\s*(-?\d+)\s+do\s*$', line)
+ if m:
+ var, start, end = m.group(1), int(m.group(2)), int(m.group(3))
+ body = []
+ depth = 1
+ j = i + 1
+ while j < n and depth > 0:
+ bline = lines[j].strip()
+ if bline == "end":
+ depth -= 1
+ if depth == 0:
+ break
+ elif re.match(r'^(for|if|function)\b', bline) and \
+ (bline.endswith("do") or bline.endswith("then")):
+ depth += 1
+ if depth > 0:
+ body.append(bline)
+ j += 1
+ for val in range(start, end + 1):
+ loop_env = dict(env)
+ loop_env[var] = val
+ for bline in body:
+ lm = re.match(r'^local\s+(\w+)\s*=\s*(.+)$', bline)
+ if lm:
+ result = _lua_simple_arith(lm.group(2), loop_env)
+ if result is not None:
+ loop_env[lm.group(1)] = result
+ continue
+ _lua_try_add_bind(bline, loop_env, binds)
+ i = j + 1
+ continue
+
+ _lua_try_add_bind(line, env, binds)
+ i += 1
+
+ return binds
+
+
+# ─── Page: Hyprland Keybinds ─────────────────────────────────────────────────
+
+class HyprlandPage(QWidget):
+ def __init__(self):
+ super().__init__()
+ cfg = HYPR_CONFIG if HYPR_CONFIG.exists() else _HYPR_FALLBACK
+ self._binds = _parse_hypr_binds(cfg)
+ self._cfg_path = cfg
+ if not self._binds:
+ # nincs hagyomanyos hyprland.conf (vagy nincs benne bind sor) -
+ # a RaveOS Hyprland 0.55+ Lua config modot hasznalja
+ lua_cfg = HYPR_KEYBINDS_LUA if HYPR_KEYBINDS_LUA.exists() else _HYPR_KEYBINDS_LUA_FALLBACK
+ lua_binds = _parse_hypr_binds_lua(lua_cfg)
+ if lua_binds:
+ self._binds = lua_binds
+ self._cfg_path = lua_cfg
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 28, 60, 20)
+ lay.setSpacing(10)
+
+ header = QHBoxLayout()
+ self.title = QLabel(_t("hypr_title"))
+ self.title.setObjectName("section_title")
+ header.addWidget(self.title)
+ header.addStretch()
+ self.src = QLabel(_t("hypr_source") + str(self._cfg_path))
+ self.src.setStyleSheet(f"font-size: 10px; color: {COLORS['text_dim']};")
+ header.addWidget(self.src)
+ lay.addLayout(header)
+
+ if not self._binds:
+ self.msg = QLabel(_t("hypr_none"))
+ self.msg.setObjectName("body")
+ self.msg.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.msg, alignment=Qt.AlignmentFlag.AlignCenter)
+ lay.addStretch()
+ return
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
+ scroll.setStyleSheet("background: transparent;")
+
+ inner = QWidget()
+ inner.setStyleSheet("background: transparent;")
+ vlay = QVBoxLayout(inner)
+ vlay.setContentsMargins(0, 4, 8, 0)
+ vlay.setSpacing(0)
+
+ card = QFrame()
+ card.setObjectName("card")
+ card_lay = QVBoxLayout(card)
+ card_lay.setContentsMargins(16, 10, 16, 10)
+ card_lay.setSpacing(0)
+
+ for i, (key, desc) in enumerate(self._binds):
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 6, 0, 6)
+ row.setSpacing(12)
+
+ key_lbl = QLabel(key)
+ key_lbl.setFixedWidth(230)
+ key_lbl.setStyleSheet(
+ f"font-family: monospace; font-size: 12px; font-weight: 600;"
+ f" color: {COLORS['accent_light']}; background: {COLORS['surface2']};"
+ f" border-radius: 4px; padding: 3px 8px;"
+ )
+
+ desc_lbl = QLabel(desc)
+ desc_lbl.setStyleSheet(f"font-size: 13px; color: {COLORS['text']};")
+
+ row.addWidget(key_lbl)
+ row.addWidget(desc_lbl)
+ row.addStretch()
+ card_lay.addLayout(row)
+
+ if i < len(self._binds) - 1:
+ div = QFrame()
+ div.setFixedHeight(1)
+ div.setStyleSheet(f"background: {COLORS['border']}; border: none;")
+ card_lay.addWidget(div)
+
+ vlay.addWidget(card)
+ vlay.addStretch()
+ scroll.setWidget(inner)
+ lay.addWidget(scroll, 1)
+
+ def retranslate(self):
+ self.title.setText(_t("hypr_title"))
+ self.src.setText(_t("hypr_source") + str(self._cfg_path))
+ if not self._binds:
+ self.msg.setText(_t("hypr_none"))
+
+
+# ─── GPU Detection ────────────────────────────────────────────────────────────
+
+def _detect_gpu() -> str:
+ try:
+ out = subprocess.check_output(
+ ["lspci"], stderr=subprocess.DEVNULL
+ ).decode().lower()
+ if "amd" in out or "radeon" in out:
+ return "amd"
+ if "nvidia" in out:
+ return "nvidia"
+ except Exception:
+ pass
+ return "other"
+
+
+# ─── Page 3: Optimize ──────────────────────────────────────────────────────────
+
+OPTIMIZE_SCRIPT = "/usr/share/raveos-welcome/gaming-optimize.sh"
+OPTIMIZE_LOG = "/var/log/raveos-welcome/gaming-optimize.log"
+
+CHECKBOX_STYLE = f"""
+QCheckBox {{
+ color: {COLORS['text']};
+ font-size: 13px;
+ spacing: 8px;
+}}
+QCheckBox::indicator {{
+ width: 16px;
+ height: 16px;
+ border-radius: 3px;
+ border: 1px solid {COLORS['border']};
+ background: {COLORS['surface2']};
+}}
+QCheckBox::indicator:checked {{
+ background: {COLORS['accent']};
+ border-color: {COLORS['accent']};
+}}
+"""
+
+
+class OptimizePage(QWidget):
+ def __init__(self):
+ super().__init__()
+ self._gpu = _detect_gpu()
+ self._checks = {}
+ self._hints = {}
+ self._proc = None
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 28, 60, 20)
+ lay.setSpacing(10)
+
+ self.title = QLabel(_t("opt_title"))
+ self.title.setObjectName("section_title")
+ lay.addWidget(self.title)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
+ scroll.setStyleSheet("background: transparent;")
+
+ inner = QWidget()
+ inner.setStyleSheet("background: transparent;")
+ vlay = QVBoxLayout(inner)
+ vlay.setContentsMargins(0, 0, 8, 0)
+ vlay.setSpacing(4)
+
+ # --- Browsers ---
+ self.sec1 = QLabel(_t("opt_browser"))
+ self.sec1.setObjectName("section_title")
+ vlay.addWidget(self.sec1)
+ vlay.addSpacing(4)
+
+ browsers = [
+ ("brave", "opt_brave", "opt_brave_desc"),
+ ("firefox", "opt_firefox", "opt_firefox_desc"),
+ ]
+ self._browser_checks = {}
+ for key, label_key, desc_key in browsers:
+ cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=False)
+ self._browser_checks[key] = (label_key, desc_key, cb)
+
+ cb_bp = self._add_check(vlay, "brave_profile",
+ _t("opt_brave_profile"),
+ _t("opt_brave_profile_desc"),
+ default=False)
+ self._brave_profile_check = ("opt_brave_profile", "opt_brave_profile_desc", cb_bp)
+
+ vlay.addSpacing(14)
+
+ # --- Gaming optimization ---
+ gpu_name = {"amd": "AMD", "nvidia": "Nvidia"}.get(self._gpu, "Ismeretlen")
+ self.sec2 = QLabel(_t("opt_gaming", gpu=gpu_name))
+ self.sec2.setObjectName("section_title")
+ vlay.addWidget(self.sec2)
+ vlay.addSpacing(4)
+
+ common = [
+ ("sysctl", "opt_sysctl", "opt_sysctl_desc"),
+ ("io_sched", "opt_io_sched", "opt_io_sched_desc"),
+ ("ananicy", "opt_ananicy", "opt_ananicy_desc"),
+ ("gamemode", "opt_gamemode", "opt_gamemode_desc"),
+ ]
+ self._common_checks = {}
+ for key, label_key, desc_key in common:
+ cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=True)
+ self._common_checks[key] = (label_key, desc_key, cb)
+
+ self._amd_checks = {}
+ self._nvidia_checks = {}
+ if self._gpu == "amd":
+ amd_opts = [
+ ("gpu_profile", "opt_gpu_profile", "opt_gpu_profile_desc"),
+ ("amd_overdrive", "opt_amd_overdrive", "opt_amd_overdrive_desc"),
+ ("amd_powercap", "opt_amd_powercap", "opt_amd_powercap_desc"),
+ ]
+ for key, label_key, desc_key in amd_opts:
+ cb = self._add_check(vlay, key, _t(label_key), _t(desc_key), default=(key in ("gpu_profile", "amd_powercap")))
+ self._amd_checks[key] = (label_key, desc_key, cb)
+
+ elif self._gpu == "nvidia":
+ cb = self._add_check(vlay,
+ "nvidia_perf", _t("opt_nvidia_perf"),
+ _t("opt_nvidia_perf_desc"),
+ default=True)
+ self._nvidia_checks["nvidia_perf"] = ("opt_nvidia_perf", "opt_nvidia_perf_desc", cb)
+
+ vlay.addStretch()
+ scroll.setWidget(inner)
+ lay.addWidget(scroll, 1)
+
+ # Status + gomb
+ self.status_lbl = QLabel("")
+ self.status_lbl.setObjectName("body")
+ self.status_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.status_lbl)
+
+ self.progress = QProgressBar()
+ self.progress.setRange(0, 0)
+ self.progress.setFixedHeight(4)
+ self.progress.setVisible(False)
+ lay.addWidget(self.progress)
+
+ self.apply_btn = QPushButton(_t("opt_apply"))
+ self.apply_btn.setObjectName("primary")
+ self.apply_btn.clicked.connect(self._apply)
+ lay.addWidget(self.apply_btn, alignment=Qt.AlignmentFlag.AlignRight)
+
+ def retranslate(self):
+ self.title.setText(_t("opt_title"))
+ self.sec1.setText(_t("opt_browser"))
+ gpu_name = {"amd": "AMD", "nvidia": "Nvidia"}.get(self._gpu, "Ismeretlen")
+ self.sec2.setText(_t("opt_gaming", gpu=gpu_name))
+ self.apply_btn.setText(_t("opt_apply"))
+ for key, (lk, dk, cb) in self._browser_checks.items():
+ cb.setText(_t(lk))
+ if key in self._hints:
+ self._hints[key].setToolTip(_t(dk))
+ bp_lk, bp_dk, bp_cb = self._brave_profile_check
+ bp_cb.setText(_t(bp_lk))
+ if "brave_profile" in self._hints:
+ self._hints["brave_profile"].setToolTip(_t(bp_dk))
+ for key, (lk, dk, cb) in self._common_checks.items():
+ cb.setText(_t(lk))
+ if key in self._hints:
+ self._hints[key].setToolTip(_t(dk))
+ for key, (lk, dk, cb) in self._amd_checks.items():
+ cb.setText(_t(lk))
+ if key in self._hints:
+ self._hints[key].setToolTip(_t(dk))
+ for key, (lk, dk, cb) in self._nvidia_checks.items():
+ cb.setText(_t(lk))
+ if key in self._hints:
+ self._hints[key].setToolTip(_t(dk))
+
+ def _add_check(self, layout, key, label, desc, default):
+ row = QHBoxLayout()
+ row.setContentsMargins(0, 0, 0, 0)
+ row.setSpacing(6)
+
+ cb = QCheckBox(label)
+ cb.setChecked(default)
+ cb.setStyleSheet(CHECKBOX_STYLE)
+ row.addWidget(cb)
+
+ hint = QLabel("?")
+ hint.setToolTip(desc)
+ hint.setCursor(Qt.CursorShape.WhatsThisCursor)
+ hint.setFixedSize(16, 16)
+ hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ hint.setStyleSheet(
+ f"color: {COLORS['text_dim']}; font-size: 11px; font-weight: 600;"
+ f" background: {COLORS['surface2']}; border-radius: 8px;"
+ f" border: 1px solid {COLORS['border']};"
+ )
+ row.addWidget(hint)
+ row.addStretch()
+
+ layout.addLayout(row)
+ layout.addSpacing(3)
+ self._checks[key] = cb
+ self._hints[key] = hint
+ return cb
+
+ def _apply(self):
+ selected = [k for k, cb in self._checks.items() if cb.isChecked()]
+ if not selected:
+ self.status_lbl.setStyleSheet(f"color: {COLORS['warning']};")
+ self.status_lbl.setText(_t("opt_apply_empty"))
+ return
+
+ script = OPTIMIZE_SCRIPT
+ if not Path(script).exists():
+ self.status_lbl.setStyleSheet(f"color: {COLORS['error']};")
+ self.status_lbl.setText(_t("opt_apply_missing") + script)
+ return
+
+ self.apply_btn.setEnabled(False)
+ self.status_lbl.setText("")
+ self.progress.setVisible(True)
+
+ self._proc = QProcess(self)
+ self._proc.finished.connect(self._on_done)
+ self._proc.start("pkexec", [script] + selected)
+
+ def _on_done(self, exit_code, _):
+ self.apply_btn.setEnabled(True)
+ self.progress.setVisible(False)
+ if exit_code == 0:
+ self.status_lbl.setStyleSheet(f"color: {COLORS['success']};")
+ self.status_lbl.setText(_t("opt_apply_ok"))
+ else:
+ self.status_lbl.setStyleSheet(f"color: {COLORS['error']};")
+ self.status_lbl.setText(_t("opt_apply_err", code=exit_code, log=OPTIMIZE_LOG))
+
+
+# ─── Page 4: Credits ──────────────────────────────────────────────────────────
+
+class CreditsPage(QWidget):
+ def __init__(self):
+ super().__init__()
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 40, 60, 40)
+ lay.setSpacing(0)
+
+ self.title = QLabel(_t("credits_title"))
+ self.title.setObjectName("section_title")
+ self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.title)
+
+ sep = QFrame()
+ sep.setObjectName("separator")
+ sep.setFixedHeight(1)
+ lay.addSpacing(16)
+ lay.addWidget(sep)
+ lay.addSpacing(24)
+
+ people = [
+ ("credits_rp1", "credits_rp1_name", "credits_rp1_role", "credits_rp1_desc"),
+ ("credits_alexc", "credits_alexc_name", "credits_alexc_role", "credits_alexc_desc"),
+ ("credits_nippy", "credits_nippy_name", "credits_nippy_role", "credits_nippy_desc"),
+ ("credits_gabesz","credits_gabesz_name","credits_gabesz_role","credits_gabesz_desc"),
+ ("credits_stefi", "credits_stefi_name", "credits_stefi_role", "credits_stefi_desc"),
+ ]
+
+ self._people_cards = []
+ for prefix, name_key, role_key, desc_key in people:
+ card = QFrame()
+ card.setObjectName("card")
+ card_lay = QHBoxLayout(card)
+ card_lay.setContentsMargins(20, 14, 20, 14)
+ card_lay.setSpacing(16)
+
+ left = QVBoxLayout()
+ left.setSpacing(2)
+ name_lbl = QLabel(_t(name_key))
+ name_lbl.setStyleSheet(
+ f"font-size: 15px; font-weight: 700; color: {COLORS['text']};"
+ )
+ role_lbl = QLabel(_t(role_key))
+ role_lbl.setStyleSheet(
+ f"font-size: 11px; font-weight: 600; letter-spacing: 1px;"
+ f" color: {COLORS['accent_light']};"
+ )
+ left.addWidget(name_lbl)
+ left.addWidget(role_lbl)
+
+ desc_lbl = QLabel(_t(desc_key))
+ desc_lbl.setStyleSheet(
+ f"font-size: 12px; color: {COLORS['text_dim']}; font-style: italic;"
+ )
+ desc_lbl.setWordWrap(True)
+ desc_lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+
+ card_lay.addLayout(left)
+ card_lay.addStretch()
+ card_lay.addWidget(desc_lbl)
+
+ lay.addWidget(card)
+ lay.addSpacing(10)
+
+ self._people_cards.append((name_key, role_key, desc_key, name_lbl, role_lbl, desc_lbl))
+
+ lay.addStretch()
+
+ def retranslate(self):
+ self.title.setText(_t("credits_title"))
+ for name_key, role_key, desc_key, name_lbl, role_lbl, desc_lbl in self._people_cards:
+ name_lbl.setText(_t(name_key))
+ role_lbl.setText(_t(role_key))
+ desc_lbl.setText(_t(desc_key))
+
+
+# ─── Page 5: Finish ────────────────────────────────────────────────────────────
+
+class FinishPage(QWidget):
+ launch_installer = pyqtSignal()
+
+ def __init__(self):
+ super().__init__()
+ self._build()
+
+ def _build(self):
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(60, 40, 60, 40)
+ lay.setSpacing(0)
+
+ self.done_label = QLabel(_t("finish_ok"))
+ self.done_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.done_label.setStyleSheet(
+ f"font-size: 52px; font-weight: 700; color: {COLORS['success']};"
+ f"font-family: Ubuntu; padding: 10px;"
+ )
+ lay.addWidget(self.done_label)
+ lay.addSpacing(12)
+
+ self.title = QLabel(_t("finish_title"))
+ self.title.setObjectName("title")
+ self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.title)
+ lay.addSpacing(6)
+
+ self.sub = QLabel(_t("finish_sub"))
+ self.sub.setObjectName("subtitle")
+ self.sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ lay.addWidget(self.sub)
+
+ sep = QFrame()
+ sep.setObjectName("separator")
+ sep.setFixedHeight(1)
+ lay.addSpacing(28)
+ lay.addWidget(sep)
+ lay.addSpacing(24)
+
+ # App installer CTA card
+ card = QFrame()
+ card.setObjectName("card")
+ card_lay = QVBoxLayout(card)
+ card_lay.setContentsMargins(24, 18, 24, 18)
+ card_lay.setSpacing(10)
+
+ self.card_title = QLabel(_t("finish_next_step"))
+ self.card_title.setObjectName("section_title")
+ card_lay.addWidget(self.card_title)
+
+ self.card_body = QLabel(_t("finish_inst_desc"))
+ self.card_body.setObjectName("body")
+ self.card_body.setWordWrap(True)
+ card_lay.addWidget(self.card_body)
+
+ self.install_btn = QPushButton(_t("finish_install"))
+ self.install_btn.setObjectName("primary")
+ self.install_btn.clicked.connect(self.launch_installer.emit)
+ card_lay.addWidget(self.install_btn, alignment=Qt.AlignmentFlag.AlignRight)
+
+ self.install_status = QLabel("")
+ self.install_status.setObjectName("body")
+ self.install_status.setWordWrap(True)
+ self.install_status.setVisible(False)
+ card_lay.addWidget(self.install_status)
+
+ lay.addWidget(card)
+ lay.addStretch()
+
+ def retranslate(self):
+ self.done_label.setText(_t("finish_ok"))
+ self.title.setText(_t("finish_title"))
+ self.sub.setText(_t("finish_sub"))
+ self.card_title.setText(_t("finish_next_step"))
+ self.card_body.setText(_t("finish_inst_desc"))
+ self.install_btn.setText(_t("finish_install"))
+
+ def show_error(self, msg):
+ self.install_btn.setEnabled(True)
+ self.install_status.setStyleSheet(f"color: {COLORS['error']};")
+ self.install_status.setText(msg)
+ self.install_status.setVisible(True)
+
+
+# ─── Main Window ───────────────────────────────────────────────────────────────
+
+class RaveOSWelcome(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("RaveOS — Üdvözlünk")
+ self.setMinimumSize(720, 640)
+ self.resize(800, 720)
+ self.setStyleSheet(STYLESHEET)
+ self._show_wifi = _should_show_wifi()
+ self._show_hyprland = _detect_hyprland()
+
+ central = QWidget()
+ central.setObjectName("central")
+ self.setCentralWidget(central)
+
+ root = QVBoxLayout(central)
+ root.setContentsMargins(0, 0, 0, 0)
+ root.setSpacing(0)
+
+ # Step indicator bar
+ self.step_bar = self._make_step_bar()
+ root.addWidget(self.step_bar)
+
+ # Page container
+ self.stack = QStackedWidget()
+ root.addWidget(self.stack, stretch=1)
+
+ # Pages
+ self.p_welcome = WelcomePage()
+ self.p_optimize = OptimizePage()
+ self.p_credits = CreditsPage()
+ self.p_finish = FinishPage()
+
+ self.stack.addWidget(self.p_welcome)
+ if self._show_wifi:
+ self.p_wifi = WiFiPage()
+ self.stack.addWidget(self.p_wifi)
+ self.stack.addWidget(self.p_optimize)
+ if self._show_hyprland:
+ self.p_hyprland = HyprlandPage()
+ self.stack.addWidget(self.p_hyprland)
+ self.stack.addWidget(self.p_credits)
+ self.stack.addWidget(self.p_finish)
+
+ # Nav buttons
+ nav = QHBoxLayout()
+ nav.setContentsMargins(24, 10, 24, 18)
+ nav.setSpacing(12)
+
+ # Language selector (bottom-left)
+ self.lang_hu_btn = QPushButton("Magyar")
+ self.lang_hu_btn.setObjectName("lang_btn")
+ self.lang_hu_btn.setFixedHeight(30)
+ self.lang_hu_btn.setCheckable(True)
+ self.lang_hu_btn.setChecked(True)
+ self.lang_hu_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;"
+ f" border: none; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}"
+ )
+ self.lang_hu_btn.clicked.connect(lambda: self._set_lang("hu"))
+ nav.addWidget(self.lang_hu_btn)
+
+ self.lang_en_btn = QPushButton("English")
+ self.lang_en_btn.setObjectName("lang_btn")
+ self.lang_en_btn.setFixedHeight(30)
+ self.lang_en_btn.setCheckable(True)
+ self.lang_en_btn.setChecked(False)
+ self.lang_en_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};"
+ f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}"
+ )
+ self.lang_en_btn.clicked.connect(lambda: self._set_lang("en"))
+ nav.addWidget(self.lang_en_btn)
+
+ self.back_btn = QPushButton(_t("nav_back"))
+ self.back_btn.setObjectName("secondary")
+ self.back_btn.clicked.connect(self._prev_page)
+ self.back_btn.setVisible(False)
+ nav.addWidget(self.back_btn)
+
+ nav.addStretch()
+
+ self.next_btn = QPushButton(_t("nav_next"))
+ self.next_btn.setObjectName("primary")
+ self.next_btn.clicked.connect(self._next_or_close)
+ nav.addWidget(self.next_btn)
+
+ root.addLayout(nav)
+
+ self.p_finish.launch_installer.connect(self._launch_installer)
+ self._update_nav()
+
+ def _set_lang(self, lang):
+ global _current_lang
+ if _current_lang == lang:
+ return
+ _current_lang = lang
+ self.lang_hu_btn.setChecked(lang == "hu")
+ self.lang_en_btn.setChecked(lang == "en")
+ if lang == "hu":
+ self.lang_hu_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;"
+ f" border: none; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}"
+ )
+ self.lang_en_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};"
+ f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}"
+ )
+ else:
+ self.lang_en_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['accent']}; color: #ffffff;"
+ f" border: none; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ background: {COLORS['accent_hover']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; }}"
+ )
+ self.lang_hu_btn.setStyleSheet(
+ f"QPushButton {{ background: {COLORS['surface']}; color: {COLORS['text_dim']};"
+ f" border: 1px solid {COLORS['border']}; border-radius: 4px; padding: 4px 12px;"
+ f" font-size: 11px; font-weight: 600; }}"
+ f"QPushButton:hover {{ border-color: {COLORS['accent']}; color: {COLORS['accent_light']}; }}"
+ f"QPushButton:checked {{ background: {COLORS['accent']}; color: #ffffff; border: none; }}"
+ )
+ self._retranslate_all()
+
+ def _make_step_bar(self):
+ bar = QWidget()
+ bar.setFixedHeight(48)
+ bar.setStyleSheet(
+ f"background: {COLORS['bg2']}; border-bottom: 1px solid {COLORS['border']};"
+ )
+ lay = QHBoxLayout(bar)
+ lay.setContentsMargins(24, 0, 24, 0)
+
+ self.step_labels = []
+ self._step_keys = ["step_welcome"]
+ if self._show_wifi:
+ self._step_keys.append("step_network")
+ self._step_keys.append("step_optimize")
+ if self._show_hyprland:
+ self._step_keys.append("step_hyprland")
+ self._step_keys += ["step_credits", "step_finish"]
+
+ for i, key in enumerate(self._step_keys):
+ if i > 0:
+ dot = QLabel("·····")
+ dot.setStyleSheet(f"color: {COLORS['border']}; font-size: 10px; letter-spacing: 2px;")
+ lay.addWidget(dot)
+
+ lbl = QLabel(f"{i+1} {_t(key).upper()}")
+ lbl.setStyleSheet(
+ f"font-size: 11px; font-weight: 600; letter-spacing: 2px; "
+ f"color: {COLORS['text_dim']};"
+ )
+ lay.addWidget(lbl)
+ self.step_labels.append(lbl)
+
+ lay.addStretch()
+ return bar
+
+ def _update_nav(self):
+ idx = self.stack.currentIndex()
+ self.back_btn.setVisible(idx > 0)
+ is_last = idx == self.stack.count() - 1
+ self.next_btn.setText(_t("finish_close") if is_last else _t("nav_next"))
+ self.lang_hu_btn.setVisible(idx == 0)
+ self.lang_en_btn.setVisible(idx == 0)
+ self._update_steps(idx)
+
+ def _update_steps(self, idx):
+ for i, lbl in enumerate(self.step_labels):
+ if i < idx:
+ lbl.setStyleSheet(
+ f"font-size: 11px; font-weight: 600; letter-spacing: 2px; "
+ f"color: {COLORS['success']};"
+ )
+ elif i == idx:
+ lbl.setStyleSheet(
+ f"font-size: 11px; font-weight: 700; letter-spacing: 2px; "
+ f"color: {COLORS['accent_light']};"
+ )
+ else:
+ lbl.setStyleSheet(
+ f"font-size: 11px; font-weight: 600; letter-spacing: 2px; "
+ f"color: {COLORS['text_dim']};"
+ )
+
+ def _retranslate_all(self):
+ if _current_lang == "en":
+ self.setWindowTitle("RaveOS — Welcome")
+ else:
+ self.setWindowTitle("RaveOS — Üdvözlünk")
+ self.back_btn.setText(_t("nav_back"))
+ is_last = self.stack.currentIndex() == self.stack.count() - 1
+ self.next_btn.setText(_t("finish_close") if is_last else _t("nav_next"))
+ for i, key in enumerate(self._step_keys):
+ self.step_labels[i].setText(f"{i+1} {_t(key).upper()}")
+ self.p_welcome.retranslate()
+ if self._show_wifi:
+ self.p_wifi.retranslate()
+ self.p_optimize.retranslate()
+ if self._show_hyprland:
+ self.p_hyprland.retranslate()
+ self.p_credits.retranslate()
+ self.p_finish.retranslate()
+
+ def _next_or_close(self):
+ idx = self.stack.currentIndex()
+ if idx == self.stack.count() - 1:
+ QApplication.quit()
+ else:
+ self._next_page()
+
+ def _next_page(self):
+ idx = self.stack.currentIndex()
+ if idx < self.stack.count() - 1:
+ self.stack.setCurrentIndex(idx + 1)
+ self._update_nav()
+
+ def _prev_page(self):
+ idx = self.stack.currentIndex()
+ if idx > 0:
+ self.stack.setCurrentIndex(idx - 1)
+ self._update_nav()
+
+ def _launch_installer(self):
+ candidates = [
+ APP_INSTALLER,
+ shutil.which("raveos-app-installer") or "",
+ ]
+ cmd = None
+ for c in candidates:
+ if c and Path(c).exists():
+ cmd = c
+ break
+
+ if not cmd:
+ self.p_finish.show_error(_t("finish_install_missing"))
+ return
+
+ self.p_finish.install_btn.setEnabled(False)
+
+ log_path = Path.home() / ".config" / "raveos" / "app-installer.log"
+ log_path.parent.mkdir(parents=True, exist_ok=True)
+
+ is_hyprland = bool(os.environ.get("HYPRLAND_INSTANCE_SIGNATURE")) or \
+ os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == "hyprland"
+
+ if is_hyprland:
+ # Hyprland 0.55+ Lua config mod alatt a hagyomanyos
+ # "hyprctl dispatch exec <parancs>" szintaxis mar nem mukodik,
+ # csak a hl.dsp.exec_cmd("...") Lua hivas - de a regi .conf
+ # alapu Hyprland telepiteseken meg a regi szintaxis a jo, ezert
+ # eloszor azt probaljuk, es csak hiba eseten valtunk Lua-ra.
+ result = subprocess.run(
+ ["hyprctl", "dispatch", "exec", cmd],
+ capture_output=True, text=True
+ )
+ if "error" in (result.stdout + result.stderr).lower():
+ escaped = cmd.replace('"', '\\"')
+ subprocess.run(
+ ["hyprctl", "dispatch", f'hl.dsp.exec_cmd("{escaped}")'],
+ capture_output=True, text=True
+ )
+ _write_flag()
+ self.p_finish.install_btn.setEnabled(True)
+ return
+
+ try:
+ with open(log_path, "ab") as log:
+ proc = subprocess.Popen(
+ [cmd],
+ stdout=log, stderr=log,
+ start_new_session=True
+ )
+ except OSError as e:
+ self.p_finish.show_error(_t("finish_install_err", log=str(e)))
+ return
+
+ _write_flag()
+
+ def _check():
+ code = proc.poll()
+ if code is not None and code != 0:
+ self.p_finish.show_error(_t("finish_install_err", log=str(log_path)))
+ else:
+ self.p_finish.install_btn.setEnabled(True)
+
+ QTimer.singleShot(1200, _check)
+
+
+# ─── Flag helpers ──────────────────────────────────────────────────────────────
+
+def _flag_exists() -> bool:
+ return FLAG_FILE.exists()
+
+def _write_flag():
+ FLAG_FILE.parent.mkdir(parents=True, exist_ok=True)
+ FLAG_FILE.touch()
+
+
+# ─── Fast tooltip style ────────────────────────────────────────────────────────
+
+class _FastTooltipStyle(QProxyStyle):
+ def styleHint(self, hint, option=None, widget=None, returnData=None):
+ if hint == QStyle.StyleHint.SH_ToolTip_WakeUpDelay:
+ return 100
+ if hint == QStyle.StyleHint.SH_ToolTip_FallAsleepDelay:
+ return 5000
+ return super().styleHint(hint, option, widget, returnData)
+
+
+# ─── Entry point ───────────────────────────────────────────────────────────────
+
+def main():
+ if _flag_exists() and "--force" not in sys.argv:
+ print("RaveOS Welcome: már futott, kihagyva. (--force a felülíráshoz)")
+ sys.exit(0)
+
+ app = QApplication(sys.argv)
+ app.setStyle(_FastTooltipStyle())
+ app.setApplicationName("raveos-welcome-session")
+ app.setDesktopFileName("raveos-welcome-session")
+ QFontDatabase.addApplicationFont("/usr/share/fonts/ubuntu/Ubuntu-R.ttf")
+ QFontDatabase.addApplicationFont("/usr/share/fonts/ubuntu/Ubuntu-B.ttf")
+
+ app_icon = QIcon()
+ svg_path = _find_svg()
+ if svg_path:
+ from PyQt6.QtSvg import QSvgRenderer
+ from PyQt6.QtGui import QPixmap, QPainter
+ renderer = QSvgRenderer(svg_path)
+ for size in (16, 32, 48, 64, 128, 256, 512):
+ px = QPixmap(size, size)
+ px.fill(Qt.GlobalColor.transparent)
+ p = QPainter(px)
+ renderer.render(p)
+ p.end()
+ app_icon.addPixmap(px)
+ else:
+ app_icon = QIcon.fromTheme("raveos-welcome")
+ app.setWindowIcon(app_icon)
+
+ window = RaveOSWelcome()
+ window.setWindowIcon(app_icon)
+ window.show()
+
+ ret = app.exec()
+ _write_flag()
+ sys.exit(ret)
+
+
+if __name__ == "__main__":
+ main()