diff --git a/WOPR.sh b/WOPR.sh new file mode 100644 index 0000000..c9ec881 --- /dev/null +++ b/WOPR.sh @@ -0,0 +1,1661 @@ +#!/bin/bash +set -Euo pipefail + +TARGET_DIR="$HOME/.local/share/steam-launcher" +SWITCH_BIN="$TARGET_DIR/enter-gamesmode" +RETURN_BIN="$TARGET_DIR/leave-gamesmode" + +# Try to find the user's bindings config file +BINDINGS_CONFIG="" +for location in \ + "$HOME/.config/hypr/bindings.conf" \ + "$HOME/.config/hypr/keybinds.conf" \ + "$HOME/.config/hypr/hyprland.conf"; do + if [ -f "$location" ]; then + BINDINGS_CONFIG="$location" + break + fi +done + +CONFIG_FILE="/etc/gaming-mode.conf" +[[ -f "$HOME/.gaming-mode.conf" ]] && CONFIG_FILE="$HOME/.gaming-mode.conf" +# shellcheck source=/dev/null +source "$CONFIG_FILE" 2>/dev/null || true +# Set defaults for any unset variables (works even if config partially defines them) +: "${STEAM_LAUNCH_MODE:=bigpicture}" +: "${PERFORMANCE_MODE:=enabled}" + +ADDED_BINDINGS=0 +CREATED_TARGET_DIR=0 +NEEDS_RELOGIN=0 + +info(){ echo "[*] $*"; } +err(){ echo "[!] $*" >&2; } + +die() { + local msg="$1"; local code="${2:-1}" + echo "FATAL: $msg" >&2 + logger -t gaming-mode "Installation failed: $msg" + rollback_changes + exit "$code" +} + +rollback_changes() { + [ -f "$SWITCH_BIN" ] && rm -f "$SWITCH_BIN" + [ -f "$RETURN_BIN" ] && rm -f "$RETURN_BIN" + if [ "$CREATED_TARGET_DIR" -eq 1 ] && [ -d "$TARGET_DIR" ]; then + rmdir "$TARGET_DIR" 2>/dev/null || true + fi + + # Remove added bindings + if [ "$ADDED_BINDINGS" -eq 1 ] && [ -n "$BINDINGS_CONFIG" ] && [ -f "$BINDINGS_CONFIG" ]; then + sed -i '/# Gaming Mode bindings - added by installation script/,/# End Gaming Mode bindings/d' "$BINDINGS_CONFIG" + fi +} + +validate_environment() { + command -v pacman >/dev/null || die "pacman required" + command -v hyprctl >/dev/null || die "hyprctl required" + [ -n "$BINDINGS_CONFIG" ] || die "Could not find bindings config file (checked bindings.conf, keybinds.conf, hyprland.conf)" + [ -f "$BINDINGS_CONFIG" ] || die "Bindings config file not found: $BINDINGS_CONFIG" + # Verify Hyprland is actually configured (check if any hypr config exists) + [ -d "$HOME/.config/hypr" ] || die "Hyprland config directory not found (~/.config/hypr)" +} + +check_package() { pacman -Qi "$1" &>/dev/null; } + +# Helper function: Detect Intel GPU type (xe=Arc, i915=iGPU) +# Returns: "arc", "igpu", "both", or "none" +# Sets global: has_arc, has_intel_igpu +detect_intel_gpu_type() { + has_arc=false + has_intel_igpu=false + + for card_device in /sys/class/drm/card*/device/driver; do + [[ -e "$card_device" ]] || continue + if [[ -L "$card_device" ]]; then + local driver_name=$(basename "$(readlink -f "$card_device")" 2>/dev/null) + case "$driver_name" in + xe) has_arc=true ;; + i915) has_intel_igpu=true ;; + esac + fi + done + + if $has_arc && $has_intel_igpu; then + echo "both" + elif $has_arc; then + echo "arc" + elif $has_intel_igpu; then + echo "igpu" + else + echo "none" + fi +} + +check_steam_dependencies() { + info "Checking Steam dependencies for Arch Linux..." + + # Force refresh package database to avoid 404 errors from stale mirrors + info "Force refreshing package database from all mirrors..." + sudo pacman -Syy || die "Failed to refresh package database" + + # Prompt for full system upgrade before installing dependencies + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " SYSTEM UPDATE RECOMMENDED" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " It's recommended to upgrade your system before installing" + echo " gaming dependencies to avoid package version conflicts." + echo "" + echo " This will run: sudo pacman -Syu" + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + read -p "Upgrade system now? [Y/n]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + info "Upgrading system..." + sudo pacman -Syu || die "Failed to upgrade system" + else + info "Skipping system upgrade - this may cause package conflicts" + fi + echo "" + + local -a missing_deps=() + local -a optional_deps=() + local multilib_enabled=false + + # Ensure pciutils is installed for GPU detection + if ! command -v lspci >/dev/null 2>&1; then + info "Installing pciutils for GPU detection..." + sudo pacman -S --needed --noconfirm pciutils || die "Failed to install pciutils" + fi + + # Check if multilib repository is enabled (required for 32-bit Steam libraries) + if grep -q "^\[multilib\]" /etc/pacman.conf 2>/dev/null; then + multilib_enabled=true + info "Multilib repository: enabled" + else + err "Multilib repository: NOT enabled (required for Steam)" + missing_deps+=("multilib-repository") + fi + + # Core Steam dependencies + local -a core_deps=( + "steam" # Steam client + "lib32-vulkan-icd-loader" # 32-bit Vulkan loader + "vulkan-icd-loader" # 64-bit Vulkan loader + "lib32-mesa" # 32-bit Mesa (OpenGL) + "mesa" # 64-bit Mesa + "mesa-utils" # Mesa utilities (glxinfo, etc.) + "lib32-systemd" # 32-bit systemd libs + "lib32-glibc" # 32-bit glibc + "lib32-gcc-libs" # 32-bit GCC libs + "lib32-libx11" # 32-bit X11 + "lib32-libxss" # 32-bit X screensaver + "lib32-alsa-plugins" # 32-bit ALSA + "lib32-libpulse" # 32-bit PulseAudio + "lib32-openal" # 32-bit OpenAL + "lib32-nss" # 32-bit NSS + "lib32-libcups" # 32-bit CUPS (printing) + "lib32-sdl2" # 32-bit SDL2 + "lib32-freetype2" # 32-bit fonts + "lib32-fontconfig" # 32-bit font config + "ttf-liberation" # Font package + "xdg-user-dirs" # User directories + ) + + # GPU-specific Vulkan drivers + local gpu_vendor + gpu_vendor=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "") + + # Detect GPU type(s) - system may have multiple GPUs + local has_nvidia=false has_amd=false has_intel=false has_arc=false has_intel_igpu=false + + if echo "$gpu_vendor" | grep -iq nvidia; then + has_nvidia=true + info "Detected NVIDIA GPU" + fi + if echo "$gpu_vendor" | grep -iqE 'amd|radeon|advanced micro'; then + has_amd=true + info "Detected AMD GPU" + fi + if echo "$gpu_vendor" | grep -iq intel; then + has_intel=true + local intel_type=$(detect_intel_gpu_type) + case "$intel_type" in + arc) info "Detected Intel Arc discrete GPU (xe driver)" ;; + igpu) info "Detected Intel integrated GPU (i915 driver)" ;; + both) info "Detected Intel Arc + integrated GPU" ;; + *) info "Detected Intel GPU" ;; + esac + fi + + # Intel Arc-specific version checks + if $has_intel; then + echo "" + info "Performing Intel Arc compatibility checks..." + check_mesa_version # Will die if Mesa too old + echo "" + fi + + local -a gpu_deps=() + + # NVIDIA drivers + if $has_nvidia; then + gpu_deps+=( + "nvidia-utils" # NVIDIA OpenGL/Vulkan driver + "lib32-nvidia-utils" # 32-bit NVIDIA driver + "nvidia-settings" # NVIDIA control panel + "libva-nvidia-driver" # VA-API support for NVIDIA + ) + # Check if user has nvidia, nvidia-dkms, or nvidia-open-dkms installed + if ! check_package "nvidia" && ! check_package "nvidia-dkms" && ! check_package "nvidia-open-dkms"; then + info "Note: You may need to install 'nvidia', 'nvidia-dkms', or 'nvidia-open-dkms' kernel module" + optional_deps+=("nvidia-dkms") + fi + fi + + # AMD drivers + if $has_amd; then + gpu_deps+=( + "vulkan-radeon" # AMD Vulkan driver (RADV) + "lib32-vulkan-radeon" # 32-bit AMD Vulkan driver + "libva-mesa-driver" # VA-API support for AMD + "lib32-libva-mesa-driver" # 32-bit VA-API + "mesa-vdpau" # VDPAU support + "lib32-mesa-vdpau" # 32-bit VDPAU + ) + # Optional AMD-specific packages (only if not installed) + ! check_package "xf86-video-amdgpu" && optional_deps+=("xf86-video-amdgpu") + fi + + # Intel drivers + if $has_intel; then + if $has_arc; then + # Intel Arc discrete GPUs: + # Use a lean Vulkan + compute stack to avoid unnecessary VA-API complexity. + gpu_deps+=( + "vulkan-intel" # Intel Vulkan driver (ANV) + "lib32-vulkan-intel" # 32-bit Intel Vulkan driver + "vulkan-icd-loader" # Vulkan ICD loader + "lib32-vulkan-icd-loader" # 32-bit Vulkan ICD loader + "intel-compute-runtime" # OpenCL/compute support (Arc / XE) + "level-zero-loader" # Intel oneAPI Level Zero (if available) + "intel-gpu-tools" # Intel GPU debugging and monitoring tools + ) + else + # Integrated Intel GPUs (UHD / Xe iGPU) – keep the full media stack. + gpu_deps+=( + "vulkan-intel" # Intel Vulkan driver (ANV) + "lib32-vulkan-intel" # 32-bit Intel Vulkan driver + "intel-media-driver" # VA-API for Intel (Broadwell+) + "libva-intel-driver" # VA-API for older Intel + "lib32-libva-intel-driver" # 32-bit VA-API + "intel-compute-runtime" # OpenCL/compute support + "level-zero-loader" # Intel oneAPI Level Zero (if available) + "intel-gpu-tools" # Intel GPU debugging and monitoring tools + ) + fi + fi + + # Fallback if no GPU detected + if ! $has_nvidia && ! $has_amd && ! $has_intel; then + info "GPU vendor not auto-detected, installing generic Vulkan drivers..." + gpu_deps+=("vulkan-radeon" "lib32-vulkan-radeon" "vulkan-intel" "lib32-vulkan-intel") + fi + + # Common Vulkan tools for all GPUs + gpu_deps+=( + "vulkan-tools" # vkcube and vulkaninfo + "vulkan-mesa-layers" # Mesa Vulkan layers + ) + + # Optional but recommended packages + local -a recommended_deps=( + "gamemode" # System optimization + "lib32-gamemode" # 32-bit gamemode + "gamescope" # Gaming compositor + "mangohud" # Performance overlay + "lib32-mangohud" # 32-bit MangoHud + "proton-ge-custom-bin" # Custom Proton (AUR) + "protontricks" # Proton helper + ) + + # Check core dependencies + info "Checking core Steam dependencies..." + for dep in "${core_deps[@]}"; do + if ! check_package "$dep"; then + missing_deps+=("$dep") + fi + done + + # Check GPU-specific dependencies + info "Checking GPU-specific dependencies..." + for dep in "${gpu_deps[@]}"; do + if ! check_package "$dep"; then + missing_deps+=("$dep") + fi + done + + # Check recommended dependencies + info "Checking recommended dependencies..." + for dep in "${recommended_deps[@]}"; do + if ! check_package "$dep"; then + optional_deps+=("$dep") + fi + done + + # Report findings + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " STEAM DEPENDENCY CHECK RESULTS" + echo "════════════════════════════════════════════════════════════════" + echo "" + + if [ "$multilib_enabled" = false ]; then + echo " CRITICAL: Multilib repository must be enabled!" + echo "" + echo " To enable multilib, edit /etc/pacman.conf and uncomment:" + echo " [multilib]" + echo " Include = /etc/pacman.d/mirrorlist" + echo "" + echo " Then run: sudo pacman -Syu" + echo "" + read -p "Enable multilib repository now? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + enable_multilib_repo + else + die "Multilib repository is required for Steam" + fi + fi + + # Remove "multilib-repository" from missing_deps if it was added + local -a clean_missing=() + for item in "${missing_deps[@]}"; do + [[ -n "$item" && "$item" != "multilib-repository" ]] && clean_missing+=("$item") + done + missing_deps=("${clean_missing[@]}") + + if ((${#missing_deps[@]})); then + echo " MISSING REQUIRED PACKAGES (${#missing_deps[@]}):" + for dep in "${missing_deps[@]}"; do + echo " • $dep" + done + echo "" + + read -p "Install missing required packages? [Y/n]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + info "Installing missing dependencies..." + sudo pacman -S --needed "${missing_deps[@]}" || die "Failed to install Steam dependencies" + info "Required dependencies installed successfully" + else + err "Cannot proceed without required dependencies" + die "Missing required Steam dependencies" + fi + else + info "All required Steam dependencies are installed!" + fi + + echo "" + if ((${#optional_deps[@]})); then + echo " RECOMMENDED PACKAGES (${#optional_deps[@]}):" + for dep in "${optional_deps[@]}"; do + echo " • $dep" + done + echo "" + + read -p "Install recommended packages? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + info "Installing recommended packages..." + # Filter out AUR packages (proton-ge-custom-bin needs AUR helper) + local -a pacman_optional=() + local -a aur_optional=() + for dep in "${optional_deps[@]}"; do + if pacman -Si "$dep" &>/dev/null; then + pacman_optional+=("$dep") + else + aur_optional+=("$dep") + fi + done + + if ((${#pacman_optional[@]})); then + sudo pacman -S --needed --noconfirm "${pacman_optional[@]}" || info "Some optional packages failed to install" + fi + + if ((${#aur_optional[@]})); then + echo "" + info "The following packages are from AUR and need an AUR helper:" + for dep in "${aur_optional[@]}"; do + echo " • $dep" + done + echo "" + + # Check for AUR helpers + if command -v yay >/dev/null 2>&1; then + read -p "Install AUR packages with yay? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + yay -S --needed --noconfirm "${aur_optional[@]}" || info "Some AUR packages failed to install" + fi + elif command -v paru >/dev/null 2>&1; then + read -p "Install AUR packages with paru? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + paru -S --needed --noconfirm "${aur_optional[@]}" || info "Some AUR packages failed to install" + fi + else + info "No AUR helper found (yay/paru). Install manually if desired." + fi + fi + fi + else + info "All recommended packages are already installed!" + fi + + echo "" + echo "════════════════════════════════════════════════════════════════" + + # Additional Steam configuration checks + check_steam_config +} + +enable_multilib_repo() { + info "Enabling multilib repository..." + + # Backup pacman.conf + sudo cp /etc/pacman.conf "/etc/pacman.conf.backup.$(date +%Y%m%d%H%M%S)" || die "Failed to backup pacman.conf" + + # Enable multilib by uncommenting the lines + sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf || die "Failed to enable multilib" + + # Verify it was enabled + if grep -q "^\[multilib\]" /etc/pacman.conf 2>/dev/null; then + info "Multilib repository enabled successfully" + echo "" + info "Updating system to enable multilib packages..." + read -p "Proceed with system upgrade? [Y/n]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + sudo pacman -Syu || die "Failed to update and upgrade system" + else + die "System upgrade required after enabling multilib - run 'sudo pacman -Syu' and try again" + fi + else + die "Failed to enable multilib repository" + fi +} + +check_steam_config() { + info "Checking Steam configuration..." + + # Check for proper permissions and offer to add user to groups + local missing_groups=() + + if ! groups | grep -q '\bvideo\b'; then + missing_groups+=("video") + fi + + if ! groups | grep -q '\binput\b'; then + missing_groups+=("input") + fi + + if ((${#missing_groups[@]})); then + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " USER GROUP PERMISSIONS" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " Your user needs to be added to the following groups:" + echo "" + for group in "${missing_groups[@]}"; do + case "$group" in + video) + echo " • video - Required for GPU hardware access" + ;; + input) + echo " • input - Required for controller/gamepad support" + ;; + esac + done + echo "" + echo " Without these groups, you may experience:" + echo " - GPU access issues" + echo " - Controller/gamepad not working in games" + echo "" + echo " NOTE: After adding groups, you MUST log out and log back in" + echo " for the changes to take effect." + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + + read -p "Add user to ${missing_groups[*]} group(s)? [Y/n]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + # Join groups with comma for usermod + local groups_to_add=$(IFS=,; echo "${missing_groups[*]}") + + info "Adding user to groups: $groups_to_add" + if sudo usermod -aG "$groups_to_add" "$USER"; then + info "Successfully added user to group(s): $groups_to_add" + NEEDS_RELOGIN=1 + else + err "Failed to add user to groups" + info "You can manually add yourself with: sudo usermod -aG $groups_to_add $USER" + fi + else + info "Skipping group assignment - you may encounter GPU/controller issues" + fi + else + info "User is in video and input groups - permissions OK" + fi + + # Check for Steam directory + if [ -d "$HOME/.steam" ]; then + info "Steam directory found at ~/.steam" + fi + + if [ -d "$HOME/.local/share/Steam" ]; then + info "Steam data directory found at ~/.local/share/Steam" + fi + + # Check for Proton/Wine dependencies + if check_package "wine"; then + info "Wine is installed (helps with some Windows games)" + else + info "Tip: Install 'wine' for better Windows game compatibility" + fi + + # Check for controller support + if check_package "steam-native-runtime"; then + info "Steam native runtime installed (better compatibility)" + fi + + # Check kernel parameters for gaming + if [ -f /proc/sys/vm/swappiness ]; then + local swappiness + swappiness=$(cat /proc/sys/vm/swappiness) + if [ "$swappiness" -gt 10 ]; then + info "Tip: Consider lowering vm.swappiness to 10 for better gaming performance" + info " Current value: $swappiness" + fi + fi + + # Check for esync/fsync support + local max_files + max_files=$(ulimit -n 2>/dev/null || echo "0") + if [ "$max_files" -lt 524288 ]; then + info "Tip: Increase open file limit for esync support" + info " Add to /etc/security/limits.conf:" + info " * hard nofile 524288" + info " * soft nofile 524288" + fi +} + +check_mesa_version() { + local mesa_version="" + + # Try to get Mesa version from multiple sources + if command -v glxinfo >/dev/null 2>&1; then + # Extract Mesa version without Perl regex + mesa_version=$(glxinfo 2>/dev/null | grep "OpenGL version string" | sed -n 's/.*Mesa \([0-9]\+\.[0-9]\+\).*/\1/p' | head -n1) + fi + + # Fallback: check package version + if [ -z "$mesa_version" ]; then + mesa_version=$(pacman -Q mesa 2>/dev/null | awk '{print $2}' | cut -d'-' -f1) + fi + + if [ -z "$mesa_version" ]; then + info "Could not detect Mesa version - will install latest from repositories" + return 0 + fi + + # Strip epoch prefix if present (e.g., "1:25.0.0" -> "25.0.0") + mesa_version=$(echo "$mesa_version" | sed 's/^[0-9]*://') + + local major minor + major=$(echo "$mesa_version" | cut -d'.' -f1) + minor=$(echo "$mesa_version" | cut -d'.' -f2) + + local version_number=$((major * 100 + minor)) + local required_version=2202 # 22.2 + local recommended_version=2300 # 23.0 + + if [ "$version_number" -lt "$required_version" ]; then + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " ⚠️ MESA VERSION WARNING" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " Current Mesa: $mesa_version" + echo " Required for Intel Arc: 22.2 or higher" + echo " Recommended: 23.0 or higher" + echo "" + echo " Intel Arc GPUs require Mesa 22.2+ for proper Vulkan support." + echo " Your current version is too old and will cause issues." + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + die "Mesa version too old for Intel Arc. Update your system: sudo pacman -Syu" + elif [ "$version_number" -lt "$recommended_version" ]; then + info "Mesa $mesa_version detected (meets minimum 22.2, but 23.0+ recommended)" + else + info "Mesa version $mesa_version meets Intel Arc recommendations (23.0+)" + fi + + return 0 +} + +setup_performance_permissions() { + local udev_rules_file="/etc/udev/rules.d/99-gaming-performance.rules" + + # Check if udev rules already exist + if [ -f "$udev_rules_file" ]; then + info "Performance udev rules already installed" + return 0 + fi + + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " ⚠️ PERFORMANCE PERMISSIONS SETUP" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " To avoid sudo password prompts during gaming, we need to set" + echo " up permissions for CPU and GPU performance control." + echo "" + echo " This will create udev rules to allow users to modify:" + echo " • CPU governor (performance mode)" + echo " • GPU performance settings (AMD/Intel/NVIDIA)" + echo "" + echo " This is a one-time setup that requires sudo." + echo " After this, gaming mode will launch without password prompts." + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + + read -p "Set up passwordless performance controls? [Y/n]: " -n 1 -r + echo + + if [[ $REPLY =~ ^[Nn]$ ]]; then + info "Skipping udev rules setup" + info "Note: You'll need to enter your password each time you launch gaming mode" + return 0 + fi + + info "Creating udev rules for performance control..." + + # Create the udev rules file + sudo tee "$udev_rules_file" > /dev/null <<'UDEV_RULES' +# Gaming Mode Performance Control Rules +# Allow users to modify CPU governor and GPU performance settings without sudo + +# CPU governor control (all CPUs) +KERNEL=="cpu[0-9]*", SUBSYSTEM=="cpu", ACTION=="add", RUN+="/bin/chmod 666 /sys/devices/system/cpu/%k/cpufreq/scaling_governor" + +# AMD GPU performance control +KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="amdgpu", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/device/power_dpm_force_performance_level" + +# Intel GPU frequency control (i915 driver - NOT used for xe driver GPUs) +# Note: This script ignores i915 GPUs at runtime to prevent conflicts with xe driver +KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="i915", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/gt_boost_freq_mhz" +KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="i915", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/gt_min_freq_mhz" +KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="i915", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/gt_max_freq_mhz" + +# Intel Arc (Xe) GPU control - NOT CURRENTLY FUNCTIONAL +# The xe driver does not expose user-controllable frequency knobs in sysfs yet +# Leaving these here for future kernel support +KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="xe", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/device/power/control" +UDEV_RULES + + if [ $? -ne 0 ]; then + err "Failed to create udev rules" + info "You can manually create $udev_rules_file later" + return 1 + fi + + info "Udev rules created successfully" + + # Reload udev rules + info "Reloading udev rules..." + sudo udevadm control --reload-rules || info "Failed to reload udev rules" + sudo udevadm trigger --subsystem-match=cpu --subsystem-match=drm || info "Failed to trigger udev" + + # Apply permissions immediately (don't wait for reboot) + info "Applying permissions immediately..." + + # CPU governor permissions + if [ -d /sys/devices/system/cpu/cpu0/cpufreq ]; then + sudo chmod 666 /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null || \ + info "Could not set CPU governor permissions (will work after reboot)" + fi + + # Detect GPU and apply permissions + local gpu_vendor + gpu_vendor=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "") + + # AMD GPU permissions + if echo "$gpu_vendor" | grep -iqE 'amd|radeon|advanced micro'; then + sudo chmod 666 /sys/class/drm/card*/device/power_dpm_force_performance_level 2>/dev/null || \ + info "Could not set AMD GPU permissions (will work after reboot)" + fi + + # Intel i915 GPU permissions (will be ignored at runtime) + if echo "$gpu_vendor" | grep -iq intel; then + sudo chmod 666 /sys/class/drm/card*/gt_boost_freq_mhz 2>/dev/null || true + sudo chmod 666 /sys/class/drm/card*/gt_min_freq_mhz 2>/dev/null || true + sudo chmod 666 /sys/class/drm/card*/gt_max_freq_mhz 2>/dev/null || true + fi + + info "✓ Performance permissions configured" + info "Gaming mode will now launch without password prompts" + + return 0 +} + +setup_intel_arc_workarounds() { + # Fix GTK4 rendering issues on Intel Arc GPUs (Nautilus, GNOME apps) + # Force OpenGL renderer instead of Vulkan to prevent visual glitches + + local intel_type=$(detect_intel_gpu_type) + [[ "$intel_type" == "arc" || "$intel_type" == "both" ]] || return 0 + + local env_dir="$HOME/.config/environment.d" + local env_file="$env_dir/10-intel-arc-gtk.conf" + + # Check if workaround already applied + if [[ -f "$env_file" ]] && grep -q "GSK_RENDERER=" "$env_file" 2>/dev/null; then + local current_renderer=$(grep "GSK_RENDERER=" "$env_file" | cut -d'=' -f2) + info "Intel Arc GTK4 workaround already configured (GSK_RENDERER=$current_renderer)" + return 0 + fi + + echo "" + info "Intel Arc GTK4 Rendering Fix (REQUIRED)" + info "Applying GSK_RENDERER=gl to fix Nautilus/GTK4 visual glitches" + info "Config: $env_file" + info "Note: Log out and log back in after installation" + echo "" + + local renderer="gl" + + # Create environment.d directory if it doesn't exist + mkdir -p "$env_dir" || die "Failed to create $env_dir" + + # Create the environment file + info "Creating GTK4 renderer configuration..." + cat > "$env_file" </dev/null 2>&1; then + # Check if capability is already set before trying to set it + if ! getcap "$(command -v gamescope)" 2>/dev/null | grep -q 'cap_sys_nice'; then + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " ⚠️ GAMESCOPE CAPABILITY REQUEST" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " Performance mode requires granting cap_sys_nice to gamescope." + echo "" + echo " This allows gamescope to:" + echo " • Run with real-time priority (--rt flag)" + echo " • Reduce latency and improve frame pacing" + echo "" + echo " Security note: This capability will be granted to ALL users" + echo " who can execute $(command -v gamescope)" + echo "" + echo " You can remove it later with:" + echo " sudo setcap -r $(command -v gamescope)" + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + read -p "Grant cap_sys_nice to gamescope? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + sudo setcap 'cap_sys_nice+ep' "$(command -v gamescope)" || info "setcap failed; --rt may be ignored." + info "cap_sys_nice granted to gamescope" + else + info "Skipping cap_sys_nice - gamescope will run without real-time priority" + info "Performance mode will still work but with slightly higher latency" + fi + else + info "Gamescope already has cap_sys_nice capability" + fi + fi +} + +deploy_launchers() { + if [ ! -d "$TARGET_DIR" ]; then + mkdir -p "$TARGET_DIR" || die "cannot create $TARGET_DIR" + CREATED_TARGET_DIR=1 + fi + + cat > "$SWITCH_BIN" <<'EOF' +#!/bin/bash +set -Euo pipefail +STATE_DIR="$HOME/.cache/gaming-session" +LOCK_FILE="$STATE_DIR/idle-prevention.lock" +SESSION_FILE="$STATE_DIR/session.pid" +CONFIG_FILE="/etc/gaming-mode.conf" +[[ -f "$HOME/.gaming-mode.conf" ]] && CONFIG_FILE="$HOME/.gaming-mode.conf" +source "$CONFIG_FILE" 2>/dev/null || true +# Set defaults for any unset variables (works even if config partially defines them) +: "${STEAM_LAUNCH_MODE:=bigpicture}" +: "${PERFORMANCE_MODE:=enabled}" + +# Cleanup function to restore system state on failure +cleanup_on_error() { + echo "[!] Error occurred, restoring system state..." >&2 + + # Restore CPU governor + if [[ -f "$STATE_DIR/original_governors" ]]; then + while IFS=: read -r cpu_path original_gov; do + [[ -z "$cpu_path" || -z "$original_gov" || "$original_gov" == "unknown" ]] && continue + if [[ -f "$cpu_path" ]]; then + echo "$original_gov" > "$cpu_path" 2>/dev/null || \ + echo "$original_gov" | sudo tee "$cpu_path" >/dev/null 2>&1 || true + fi + done < "$STATE_DIR/original_governors" + rm -f "$STATE_DIR/original_governors" + fi + + # Restore GPU performance mode + if [[ -f "$STATE_DIR/gpu_perf_mode" ]]; then + while IFS=: read -r gpu_path original_mode; do + [[ -z "$gpu_path" || -z "$original_mode" ]] && continue + [[ "$original_mode" == "nvidia_optimized" || "$original_mode" == "amd_optimized" || "$original_mode" == "xe_optimized" ]] && continue + if [[ -f "$gpu_path" ]]; then + echo "$original_mode" > "$gpu_path" 2>/dev/null || \ + echo "$original_mode" | sudo tee "$gpu_path" >/dev/null 2>&1 || true + fi + done < "$STATE_DIR/gpu_perf_mode" + # Restore NVIDIA settings + if grep -q "nvidia_optimized" "$STATE_DIR/gpu_perf_mode" 2>/dev/null; then + command -v nvidia-settings >/dev/null 2>&1 && nvidia-settings -a "[gpu:0]/GpuPowerMizerMode=0" >/dev/null 2>&1 || true + command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -pm 0 >/dev/null 2>&1 || true + fi + rm -f "$STATE_DIR/gpu_perf_mode" + fi + + # Restart hypridle only if it was running before gaming mode + if [[ -f "$STATE_DIR/hypridle_state" ]]; then + if [[ "$(cat "$STATE_DIR/hypridle_state" 2>/dev/null)" == "running" ]]; then + if command -v hypridle >/dev/null 2>&1 && ! pgrep -x hypridle >/dev/null 2>&1; then + nohup hypridle >/dev/null 2>&1 & + disown + fi + fi + rm -f "$STATE_DIR/hypridle_state" + fi + + # Clean up state files + rm -f "$LOCK_FILE" "$SESSION_FILE" 2>/dev/null || true +} + +# Set trap to cleanup on any error +trap cleanup_on_error ERR + +steam_icon() { echo "steam"; } + +# Helper function to center output in terminal +center_output() { + local width=${1:-70} + local term_width=$(tput cols) + local padding=$(( (term_width - width) / 2 )) + [[ $padding -lt 0 ]] && padding=0 + while IFS= read -r line; do + printf "%${padding}s%s\n" "" "$line" + done +} + +center_notify() { + local msg="$*" + # TUI notification using gum - centered (uses terminal theme colors) + clear + local term_width=$(tput cols) + local box_width=50 + local padding=$(( (term_width - box_width) / 2 )) + [[ $padding -lt 0 ]] && padding=0 + echo "" + gum style --align center --width 50 --border normal --padding "1 2" "$msg" | while IFS= read -r line; do printf "%${padding}s%s\\n" "" "$line"; done + sleep 2 +} + +get_display() { + local j; j="$(hyprctl monitors -j 2>/dev/null || true)" + if [[ -z "$j" ]]; then echo "1920 1080 60"; return; fi + echo "$j" | python3 -c " +import json,sys +try: + d=json.load(sys.stdin); m=d[0] if d else {} + print(int(m.get('width',1920)), int(m.get('height',1080)), int(round(m.get('refreshRate',60)))) +except Exception: + print('1920 1080 60') +" +} + +read -r horizontal_res vertical_res monitor_hz <<<"$(get_display)" +DISPLAY_WIDTH="$horizontal_res"; DISPLAY_HEIGHT="$vertical_res"; REFRESH_RATE="$monitor_hz" + +# Detect system theme and set gum colors to match Omarchy theme +detect_system_theme() { + local omarchy_theme_dir="$HOME/.config/omarchy/current/theme" + local ghostty_conf="$omarchy_theme_dir/ghostty.conf" + + # Default colors (fallback) + local accent_color="6" # Cyan (ANSI color 6) + local border_color="7" # White/light gray + local cursor_color="6" # Cyan + + # Read colors from Omarchy ghostty theme if available + if [[ -f "$ghostty_conf" ]]; then + # Extract accent color (use palette 6 or 2 as accent - typically cyan/green) + local palette_6=$(grep "^palette = 6=" "$ghostty_conf" 2>/dev/null | cut -d'=' -f3 | tr -d ' #') + local palette_2=$(grep "^palette = 2=" "$ghostty_conf" 2>/dev/null | cut -d'=' -f3 | tr -d ' #') + + if [[ -n "$palette_6" ]]; then + # Convert hex to gum color (use the hex directly) + accent_color="#$palette_6" + cursor_color="#$palette_6" + elif [[ -n "$palette_2" ]]; then + accent_color="#$palette_2" + cursor_color="#$palette_2" + fi + + # Get foreground color for borders + local fg_color=$(grep "^foreground = " "$ghostty_conf" 2>/dev/null | cut -d'=' -f2 | tr -d ' ') + if [[ -n "$fg_color" ]]; then + border_color="$fg_color" + fi + fi + + # Set gum environment variables to match theme + export GUM_CHOOSE_CURSOR_FOREGROUND="$cursor_color" + export GUM_CHOOSE_SELECTED_FOREGROUND="$accent_color" + export GUM_STYLE_BORDER_FOREGROUND="$border_color" + export GUM_SPIN_SPINNER_FOREGROUND="$accent_color" +} + +detect_system_theme + +# Check if previous settings exist +SETTINGS_FILE="$STATE_DIR/last-settings" +USE_PREVIOUS=false + +# Initialize padding for centering (used in all paths) +term_width=$(tput cols) +menu_width=75 +menu_pad=$(( (term_width - menu_width) / 2 )) +[[ $menu_pad -lt 0 ]] && menu_pad=0 +pad_str=$(printf "%${menu_pad}s" "") + +clear +echo "" +gum style --align center --width 70 --border double --padding "1 2" $'W.O.P.R\n\nShall we play a game?' | center_output 74 + +if [[ -f "$SETTINGS_FILE" ]]; then + # Ask user if they want to use previous settings + echo "" + gum style --align center --width 70 --bold "Previous settings found." | center_output 70 + echo "" + + # Calculate padding for confirm dialog + term_width=$(tput cols) + confirm_width=40 + confirm_pad=$(( (term_width - confirm_width) / 2 )) + [[ $confirm_pad -lt 0 ]] && confirm_pad=0 + confirm_pad_str=$(printf "%${confirm_pad}s" "") + + gum style --align center --width 70 "Use previous settings?" | center_output 70 + echo "" + + CONFIRM_CHOICE=$(gum choose --height 6 \ + "${confirm_pad_str}Yes - Use previous settings" \ + "${confirm_pad_str}No - Configure new settings") || CONFIRM_CHOICE="" + + if [[ "$CONFIRM_CHOICE" == *"Yes"* ]]; then + USE_PREVIOUS=true + source "$SETTINGS_FILE" + else + USE_PREVIOUS=false + fi +fi + +if [[ "$USE_PREVIOUS" == false ]]; then + # Show resolution selection menu + clear + echo "" + gum style --align center --width 70 --border double --padding "1 2" "🎮 Gaming Mode - Resolution Settings" | center_output 74 + echo "" + gum style --align center --width 70 --faint "Current Display: ${DISPLAY_WIDTH}x${DISPLAY_HEIGHT} @ ${REFRESH_RATE}Hz" | center_output 70 + echo "" + gum style --align center --width 70 "Select your preferred gaming resolution:" | center_output 70 + echo "" + + # Format native resolution option to align with others (30 chars before │) + native_res="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" + native_option=$(printf "%-30s" "$native_res") + + RESOLUTION_CHOICE=$(gum choose --height 12 \ + "${pad_str}${native_option}│ Render at native resolution (best quality)" \ + "${pad_str}1440p Upscaled │ Render at 1080p, upscale to 1440p (better FPS)" \ + "${pad_str}1440p (2560x1440) │ Render at 1440p resolution" \ + "${pad_str}UHD Upscaled │ Render at 1440p, upscale to 4K (balanced)" \ + "${pad_str}UHD (3840x2160) │ Render at 4K/UHD resolution") || { + center_notify "Gaming Mode cancelled" + exit 0 + } + + # If user cancelled, exit + if [[ -z "$RESOLUTION_CHOICE" ]]; then + center_notify "Gaming Mode cancelled" + exit 0 + fi + + # Extract just the resolution name (before the │), removing padding + RESOLUTION_CHOICE=$(echo "$RESOLUTION_CHOICE" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | xargs) + + # Convert native resolution to standard format for matching + if [[ "$RESOLUTION_CHOICE" == "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" ]]; then + RESOLUTION_CHOICE="Native (${DISPLAY_WIDTH}x${DISPLAY_HEIGHT})" + fi + + # Show MangoHud preset selection + clear + echo "" + gum style --align center --width 70 --border double --padding "1 2" "🎮 Gaming Mode - MangoHud Settings" | center_output 74 + echo "" + gum style --align center --width 70 "Select your preferred performance overlay:" | center_output 70 + echo "" + + MANGOHUD_CHOICE=$(gum choose --height 10 \ + "${pad_str}Off │ No performance overlay (cleanest view)" \ + "${pad_str}Minimal │ FPS counter only (recommended)" \ + "${pad_str}Full Stats │ Detailed metrics (CPU, GPU, temps, frametime)") || { + center_notify "Gaming Mode cancelled" + exit 0 + } + + # If user cancelled, exit + if [[ -z "$MANGOHUD_CHOICE" ]]; then + center_notify "Gaming Mode cancelled" + exit 0 + fi + + # Extract just the preset name (before the │), removing padding + MANGOHUD_CHOICE=$(echo "$MANGOHUD_CHOICE" | sed 's/^[[:space:]]*//' | sed 's/ │.*//' | xargs) + + # Save settings for next time + mkdir -p "$STATE_DIR" + cat > "$SETTINGS_FILE" < "$LOCK_FILE" + +# Save hypridle state and terminate if running +if pgrep -x hypridle >/dev/null 2>&1; then + # Mark that hypridle was running so we can restore it later + echo "running" > "$STATE_DIR/hypridle_state" + + pkill hypridle 2>/dev/null + # Wait up to 2 seconds for graceful shutdown + for i in {1..4}; do + sleep 0.5 + pgrep -x hypridle >/dev/null 2>&1 || break + done + # Force kill if still running + pkill -9 hypridle 2>/dev/null || true +else + # Mark that hypridle was not running + echo "stopped" > "$STATE_DIR/hypridle_state" +fi + +# ===== CPU GOVERNOR OPTIMIZATION ===== +# Set CPU to performance mode for lower latency and consistent frame times +if [[ -d /sys/devices/system/cpu/cpu0/cpufreq ]]; then + # Save current governor for each CPU (handles hybrid CPUs with different governors) + : > "$STATE_DIR/original_governors" + for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + if [[ -f "$cpu" ]]; then + current_gov=$(cat "$cpu" 2>/dev/null || echo "unknown") + echo "$cpu:$current_gov" >> "$STATE_DIR/original_governors" + fi + done + + # Set all CPUs to performance mode + for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + if [[ -f "$cpu" ]]; then + if [[ -w "$cpu" ]]; then + echo performance > "$cpu" 2>/dev/null || \ + echo performance | sudo tee "$cpu" >/dev/null 2>&1 + else + echo performance | sudo tee "$cpu" >/dev/null 2>&1 || true + fi + fi + done +fi + +# ===== GPU PERFORMANCE MODE ===== +# Force GPU to maximum clocks for stable performance +# Initialize GPU state file (clear any old data) +: > "$STATE_DIR/gpu_perf_mode" + +GPU_VENDOR=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "") + +# Detect all GPU types +HAS_AMD=false +HAS_NVIDIA=false +HAS_INTEL=false + +echo "$GPU_VENDOR" | grep -iqE 'amd|radeon|advanced micro' && HAS_AMD=true +echo "$GPU_VENDOR" | grep -iq nvidia && HAS_NVIDIA=true +echo "$GPU_VENDOR" | grep -iq intel && HAS_INTEL=true + +# AMD GPU optimization +if $HAS_AMD; then + for gpu in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + if [[ -f "$gpu" ]]; then + # Save current mode + current_mode=$(cat "$gpu" 2>/dev/null || echo "auto") + echo "$gpu:$current_mode" >> "$STATE_DIR/gpu_perf_mode" + + # Set to high performance + if [[ -w "$gpu" ]]; then + echo high > "$gpu" 2>/dev/null || \ + echo high | sudo tee "$gpu" >/dev/null 2>&1 + fi + fi + done + echo "amd_optimized" >> "$STATE_DIR/gpu_perf_mode" +fi + +# NVIDIA GPU optimization +if $HAS_NVIDIA; then + # nvidia-settings requires X display, may not work on pure Wayland + if command -v nvidia-settings >/dev/null 2>&1 && [[ -n "${DISPLAY:-}" || -n "${WAYLAND_DISPLAY:-}" ]]; then + # Set PowerMizer to max performance (mode 1) + if nvidia-settings -a "[gpu:0]/GpuPowerMizerMode=1" >/dev/null 2>&1; then + echo "nvidia_optimized" >> "$STATE_DIR/gpu_perf_mode" + fi + fi + # Also try nvidia-smi for persistence mode (doesn't require display) + if command -v nvidia-smi >/dev/null 2>&1; then + nvidia-smi -pm 1 >/dev/null 2>&1 || true + fi +fi + +# ===== INTEL GPU OPTIMIZATION (XE DRIVER ONLY) ===== +# IMPORTANT: This section ONLY optimizes Intel GPUs using the xe driver (Arc GPUs) +# It IGNORES i915 GPUs (integrated graphics) to prevent conflicts +# DISABLED: xe driver optimization causes visual glitches on Arc B580 with kernel 6.17 +# The xe driver power management is not mature enough for forced always-on mode +if $HAS_INTEL; then + # Check each Intel card to see which driver it uses + for card_device in /sys/class/drm/card*/device/driver; do + if [[ -L "$card_device" ]]; then + # Get the driver name + driver_name=$(basename "$(readlink -f "$card_device")") + + # Only process xe driver cards + if [[ "$driver_name" == "xe" ]]; then + # Extract card name (e.g., card0) + card_path=$(dirname "$(dirname "$card_device")") + card_name=$(basename "$card_path") + + echo "# xe driver detected on $card_name - SKIPPED (optimization disabled)" >> "$STATE_DIR/gpu_perf_mode" + + # NOTE: Power management optimization for xe driver is DISABLED + # Forcing the GPU to always-on mode causes visual glitches on Arc B580 + # with kernel 6.17.x. Let the driver manage power automatically. + # + # This may be revisited when xe driver matures in future kernels. + + elif [[ "$driver_name" == "i915" ]]; then + # Explicitly skip i915 cards + card_path=$(dirname "$(dirname "$card_device")") + card_name=$(basename "$card_path") + echo "# i915 driver detected on $card_name - SKIPPED (not optimized)" >> "$STATE_DIR/gpu_perf_mode" + fi + fi + done +fi + +# center_notify "Starting Gamesmode at ${game_width}x${game_height}…" + +gamescope_perf="" +[[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && gamescope_perf="--rt --immediate-flips" +steam_args="" +case "${STEAM_LAUNCH_MODE,,}" in + gamepadui) steam_args="-gamepadui" ;; + bigpicture|"") steam_args="-tenfoot" ;; + *) steam_args="-tenfoot" ;; +esac + +# Configure MangoHud based on user choice +mangohud_flag="" +mangohud_config="" +case "$MANGOHUD_CHOICE" in + "Off") + mangohud_flag="" + ;; + "Minimal") + mangohud_flag="--mangoapp" + mangohud_config="fps,fps_only,position=top-left,font_size=24" + ;; + "Full Stats") + mangohud_flag="--mangoapp" + mangohud_config="cpu_temp,gpu_temp,cpu_power,gpu_power,ram,vram,fps,frametime,frame_timing=1,gpu_stats,cpu_stats,position=top-left" + ;; +esac + +# Show launch status +clear +echo "" +gum style --align center --width 70 --border double --padding "1 2" "🎮 Launching Gaming Mode" | center_output 74 +echo "" +gum style --align center --width 70 --faint "Resolution: ${game_width}x${game_height}" | center_output 70 +gum style --align center --width 70 --faint "Output: ${output_width}x${output_height}" | center_output 70 +gum style --align center --width 70 --faint "Refresh: ${REFRESH_RATE}Hz" | center_output 70 +gum style --align center --width 70 --faint "MangoHud: ${MANGOHUD_CHOICE}" | center_output 70 +echo "" +echo "${pad_str}" | tr -d '\\n' +gum spin --spinner dot --title "Starting gamescope..." -- sleep 2 + +# Launch gamescope with MangoHud configuration and Gamemode optimizations +# Gamemode adds: process priority, I/O scheduling, and additional system tweaks +if [[ -n "$mangohud_config" ]]; then + export MANGOHUD_CONFIG="$mangohud_config" +fi +setsid gamemoderun /usr/bin/gamescope $gamescope_perf $mangohud_flag -f -w "$game_width" -h "$game_height" -W "$output_width" -H "$output_height" -r "$REFRESH_RATE" --force-grab-cursor -e -- /usr/bin/steam $steam_args & + +# Capture the actual PID of our gamescope instance (not all gamescope processes) +gamescope_pid=$! + +# Give gamescope a moment to initialize +sleep 2 + +# Verify gamescope started successfully +if ! kill -0 "$gamescope_pid" 2>/dev/null; then + echo "[!] ERROR: Gamescope failed to start!" >&2 + echo "[!] Restoring system state..." >&2 + cleanup_on_error + exit 1 +fi + +# Save our specific gamescope PID for cleanup +echo "$gamescope_pid" > "$SESSION_FILE" + +exit 0 +EOF + chmod +x "$SWITCH_BIN" || die "cannot chmod $SWITCH_BIN" + + cat > "$RETURN_BIN" <<'EOF' +#!/bin/bash +set -Euo pipefail +STATE_DIR="$HOME/.cache/gaming-session" +LOCK_FILE="$STATE_DIR/idle-prevention.lock" +SESSION_FILE="$STATE_DIR/session.pid" + +steam_icon() { echo "steam"; } +center_notify() { + # TUI notification - brief message on exit (uses terminal theme colors) + if command -v gum >/dev/null 2>&1; then + clear + local term_width=$(tput cols) + local box_width=50 + local padding=$(( (term_width - box_width) / 2 )) + [[ $padding -lt 0 ]] && padding=0 + echo "" + gum style --align center --width 50 --border normal --padding "1 2" "BACK TO WORK" | while IFS= read -r line; do printf "%${padding}s%s\\n" "" "$line"; done + sleep 1 + else + echo "BACK TO WORK" + sleep 1 + fi +} + +# First, gracefully terminate Steam to allow it to save state +if pgrep -x steam >/dev/null 2>&1; then + pkill -TERM steam 2>/dev/null + # Wait up to 3 seconds for graceful shutdown + for i in {1..6}; do + sleep 0.5 + pgrep -x steam >/dev/null 2>&1 || break + done + # Force kill if still running + pkill -9 steam 2>/dev/null || true +fi + +# Kill any remaining Steam processes (reaper, fossilize, etc.) +pkill -9 -f "steam" 2>/dev/null || true + +# Terminate gamescope (this should also kill Steam if it's still inside) +if [[ -f "$SESSION_FILE" ]]; then + PID="$(cat "$SESSION_FILE" 2>/dev/null || true)" + if [[ -n "$PID" ]] && kill -0 "$PID" 2>/dev/null; then + kill "$PID" 2>/dev/null || true + # Wait up to 2 seconds for graceful shutdown + for i in {1..4}; do + sleep 0.5 + kill -0 "$PID" 2>/dev/null || break + done + # Force kill if still running + kill -9 "$PID" 2>/dev/null || true + fi +fi + +# Gracefully terminate gamescope with proper verification +if pgrep -x gamescope >/dev/null 2>&1; then + pkill gamescope 2>/dev/null + # Wait up to 2 seconds for graceful shutdown + for i in {1..4}; do + sleep 0.5 + pgrep -x gamescope >/dev/null 2>&1 || break + done + # Force kill if still running + pkill -9 gamescope 2>/dev/null || true +fi + +# ===== RESTORE CPU GOVERNOR ===== +if [[ -f "$STATE_DIR/original_governors" ]]; then + while IFS=: read -r cpu_path original_gov; do + [[ -z "$cpu_path" || -z "$original_gov" || "$original_gov" == "unknown" ]] && continue + if [[ -f "$cpu_path" ]]; then + if [[ -w "$cpu_path" ]]; then + echo "$original_gov" > "$cpu_path" 2>/dev/null || \ + echo "$original_gov" | sudo tee "$cpu_path" >/dev/null 2>&1 + else + echo "$original_gov" | sudo tee "$cpu_path" >/dev/null 2>&1 || true + fi + fi + done < "$STATE_DIR/original_governors" + rm -f "$STATE_DIR/original_governors" +fi + +# ===== RESTORE GPU PERFORMANCE MODE ===== +if [[ -f "$STATE_DIR/gpu_perf_mode" ]]; then + while IFS=: read -r gpu_path original_mode; do + # Skip empty or malformed lines + [[ -z "$gpu_path" || -z "$original_mode" ]] && continue + + # Skip marker lines and comments + [[ "$original_mode" == "nvidia_optimized" || "$original_mode" == "amd_optimized" || "$original_mode" == "xe_optimized" ]] && continue + [[ "$gpu_path" == "#"* ]] && continue + + if [[ -f "$gpu_path" ]]; then + if [[ -w "$gpu_path" ]]; then + echo "$original_mode" > "$gpu_path" 2>/dev/null || \ + echo "$original_mode" | sudo tee "$gpu_path" >/dev/null 2>&1 + fi + fi + done < "$STATE_DIR/gpu_perf_mode" + + # Restore NVIDIA settings if they were changed + if grep -q "nvidia_optimized" "$STATE_DIR/gpu_perf_mode" 2>/dev/null; then + if command -v nvidia-settings >/dev/null 2>&1; then + # Set back to Adaptive mode (0) + nvidia-settings -a "[gpu:0]/GpuPowerMizerMode=0" >/dev/null 2>&1 + fi + # Disable persistence mode + if command -v nvidia-smi >/dev/null 2>&1; then + nvidia-smi -pm 0 >/dev/null 2>&1 || true + fi + fi + + rm -f "$STATE_DIR/gpu_perf_mode" +fi + +# ===== RESTART HYPRIDLE ===== +# Restart hypridle only if it was running before gaming mode +if [[ -f "$STATE_DIR/hypridle_state" ]]; then + if [[ "$(cat "$STATE_DIR/hypridle_state" 2>/dev/null)" == "running" ]]; then + if command -v hypridle >/dev/null 2>&1; then + # Check if hypridle is not already running + if ! pgrep -x hypridle >/dev/null 2>&1; then + nohup hypridle >/dev/null 2>&1 & + disown + fi + fi + fi + rm -f "$STATE_DIR/hypridle_state" +fi + +rm -f "$LOCK_FILE" "$SESSION_FILE" 2>/dev/null || true +EOF + chmod +x "$RETURN_BIN" || die "cannot chmod $RETURN_BIN" +} + +configure_shortcuts() { + info "Adding keybindings to: $BINDINGS_CONFIG" + + # Check if bindings already exist + if grep -q "# Gaming Mode bindings - added by installation script" "$BINDINGS_CONFIG" 2>/dev/null; then + info "Gaming mode bindings already exist in config, skipping..." + return 0 + fi + + # Detect the binding style used in the config + local bind_style="bindd" + if ! grep -q "^bindd[[:space:]]*=" "$BINDINGS_CONFIG" 2>/dev/null; then + if grep -q "^bind[[:space:]]*=" "$BINDINGS_CONFIG" 2>/dev/null; then + bind_style="bind" + fi + fi + + # Detect preferred terminal (ghostty preferred, fallback to others) + local terminal_cmd="ghostty -e" + local terminal_class="com.mitchellh.ghostty" + + if ! command -v ghostty >/dev/null 2>&1; then + if command -v kitty >/dev/null 2>&1; then + terminal_cmd="kitty -e" + terminal_class="kitty" + elif command -v alacritty >/dev/null 2>&1; then + terminal_cmd="alacritty -e" + terminal_class="Alacritty" + elif command -v foot >/dev/null 2>&1; then + terminal_cmd="foot -e" + terminal_class="foot" + else + terminal_cmd="xterm -e" + terminal_class="XTerm" + fi + fi + + info "Using terminal: ${terminal_cmd%% -*} (class: $terminal_class)" + + # Add bindings to the config file (TUI needs terminal with window rules) + { + echo "" + echo "# Gaming Mode bindings - added by installation script" + echo "windowrulev2 = float, class:($terminal_class)" + echo "windowrulev2 = size 800 600, class:($terminal_class)" + echo "windowrulev2 = center, class:($terminal_class)" + echo "windowrulev2 = pin, class:($terminal_class)" + if [ "$bind_style" = "bindd" ]; then + echo "bindd = SUPER SHIFT, S, Steam Gaming Mode, exec, $terminal_cmd $SWITCH_BIN" + echo "bindd = SUPER SHIFT, R, Exit Gaming Mode, exec, $RETURN_BIN" + else + echo "bind = SUPER SHIFT, S, exec, $terminal_cmd $SWITCH_BIN" + echo "bind = SUPER SHIFT, R, exec, $RETURN_BIN" + fi + echo "# End Gaming Mode bindings" + } >> "$BINDINGS_CONFIG" || die "failed to add bindings to $BINDINGS_CONFIG" + + ADDED_BINDINGS=1 + + # Reload Hyprland config + hyprctl reload >/dev/null 2>&1 || info "Hyprland reload may have failed; relog if binds inactive." +} + +check_component() { + case "$1" in + launcher) + [ -x "$SWITCH_BIN" ] && [ -x "$RETURN_BIN" ] || { + err "launcher check failed" + return 1 + } + ;; + shortcuts) + if [ ! -f "$BINDINGS_CONFIG" ] || ! grep -q "# Gaming Mode bindings" "$BINDINGS_CONFIG"; then + err "shortcuts check failed" + return 1 + fi + ;; + capabilities) + if [[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && command -v gamescope >/dev/null 2>&1; then + if ! getcap "$(command -v gamescope)" 2>/dev/null | grep -q 'cap_sys_nice'; then + info "gamescope lacks cap_sys_nice; --rt may be ignored." + fi + fi + ;; + *) + err "unknown component: $1" + return 1 + ;; + esac +} + +validate_deployment() { + local errors=0 + for component in launcher shortcuts capabilities; do + check_component "$component" || ((errors++)) + done + return $errors +} + +execute_setup() { + validate_environment + info "Found bindings config at: $BINDINGS_CONFIG" + + # Check if this is a first install or re-run + local is_reinstall=false + if [ -x "$SWITCH_BIN" ] && [ -x "$RETURN_BIN" ]; then + is_reinstall=true + fi + + # Only check dependencies on fresh install or if user requests it + if [ "$is_reinstall" = false ]; then + check_steam_dependencies + else + info "Gaming mode already installed - skipping dependency check" + echo "" + read -p "Run dependency check anyway? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + check_steam_dependencies + fi + fi + + # Handle reinstall option + if [ -x "$SWITCH_BIN" ] && [ -x "$RETURN_BIN" ]; then + # Launchers exist - offer fresh install option + info "Gaming mode launchers found" + + echo "" + read -p "Gaming mode is already installed. Reinstall? [y/N]: " -n 1 -r + echo + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + info "Installation cancelled" + exit 0 + fi + fi + + setup_requirements + deploy_launchers + configure_shortcuts + validate_deployment || die "deployment validation failed" + echo "" + info "✓ Install complete!" + info " Launch: Super+Shift+S" + info " Exit: Super+Shift+R" + info "" + info "Binaries: $TARGET_DIR" + info "Bindings: $BINDINGS_CONFIG" + info "Config: $CONFIG_FILE" + + # Check if user needs to log out/reboot for group changes + if [ "$NEEDS_RELOGIN" -eq 1 ]; then + echo "" + echo "════════════════════════════════════════════════════════════════" + echo " ⚠️ IMPORTANT: LOG OUT REQUIRED" + echo "════════════════════════════════════════════════════════════════" + echo "" + echo " User groups have been updated. You MUST log out and log back in" + echo " (or reboot) for the changes to take effect." + echo "" + echo " The gaming mode has been installed, but the group permissions" + echo " will not be active until you log out and log back in." + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + read -r -p "Press Enter to exit (remember to log out/log back in)..." + fi +} + +gaming_mode::initialize() { validate_environment; setup_requirements; } +gaming_mode::install() { deploy_launchers; } +gaming_mode::configure() { configure_shortcuts; validate_deployment || die "validation failed"; } + +execute_setup + +# Intel Arc (xe driver) Gaming Mode Installer +# - Detects and differentiates Arc (xe) vs integrated (i915) GPUs +# - Applies GSK_RENDERER=gl fix for GTK4 apps on Arc GPUs +# - Installs appropriate drivers for NVIDIA, AMD, Intel Arc, Intel iGPU