2943 lines
104 KiB
Bash
Executable file
2943 lines
104 KiB
Bash
Executable file
#!/bin/bash
|
|
# ==============================================================================
|
|
# Super Shift S - Omarchy Deck Mode Installer
|
|
#
|
|
# This script transforms an Omarchy (Arch Linux + Hyprland) desktop into a
|
|
# dual-mode system: Desktop Mode (Hyprland) and Gaming Mode (Steam Big Picture
|
|
# inside Gamescope — the same compositor the Steam Deck uses).
|
|
#
|
|
# It handles everything needed for this transformation:
|
|
# - Installing all Steam/gaming dependencies and GPU drivers
|
|
# - Configuring NVIDIA kernel parameters (nvidia-drm.modeset=1)
|
|
# - Setting up session switching between Hyprland and Gamescope via SDDM
|
|
# - Creating keybinds (Super+Shift+S to enter, Super+Shift+R to exit)
|
|
# - Configuring NetworkManager handoff (iwd <-> NM) for Steam network access
|
|
# - Setting up performance tuning (CPU governor, GPU power, kernel sysctl)
|
|
# - Auto-mounting external drives with Steam libraries
|
|
# - Configuring permissions (polkit, sudoers, udev) so it all works
|
|
# without password prompts during gameplay
|
|
#
|
|
# The script is idempotent — running it again skips steps that are already done.
|
|
# ==============================================================================
|
|
|
|
set -Euo pipefail
|
|
# -E: ERR traps are inherited by functions, command substitutions, and subshells
|
|
# -u: Treat unset variables as errors (catches typos in variable names)
|
|
# -o pipefail: A pipeline fails if ANY command in it fails, not just the last one
|
|
|
|
Super_Shift_S_VERSION="12.27"
|
|
|
|
# Load configuration from /etc/gaming-mode.conf (system-wide) or
|
|
# ~/.gaming-mode.conf (user override). This lets users disable performance
|
|
# tuning by setting PERFORMANCE_MODE=disabled without editing the script.
|
|
CONFIG_FILE="/etc/gaming-mode.conf"
|
|
[[ -f "$HOME/.gaming-mode.conf" ]] && CONFIG_FILE="$HOME/.gaming-mode.conf"
|
|
source "$CONFIG_FILE" 2>/dev/null || true
|
|
: "${PERFORMANCE_MODE:=enabled}"
|
|
|
|
# Flags that track whether the user needs to reboot or re-login after setup.
|
|
# Various steps set these to 1 when they make changes that only take effect
|
|
# after a session restart (e.g. adding user groups, changing kernel params).
|
|
NEEDS_RELOGIN=0
|
|
NEEDS_REBOOT=0
|
|
|
|
# Logging helpers — consistent prefix makes it easy to spot installer output
|
|
# in a busy terminal. err() goes to stderr so it can be captured separately.
|
|
info(){ echo "[*] $*"; }
|
|
warn(){ echo "[!] $*"; }
|
|
err(){ echo "[!] $*" >&2; }
|
|
|
|
# Fatal error handler — logs to the system journal (journalctl -t gaming-mode)
|
|
# so failures can be diagnosed even after the terminal is closed.
|
|
die() {
|
|
local msg="$1"; local code="${2:-1}"
|
|
echo "FATAL: $msg" >&2
|
|
logger -t gaming-mode "Installation failed: $msg"
|
|
exit "$code"
|
|
}
|
|
|
|
# AUR helpers (yay, paru) can break after a major system update if their
|
|
# own dependencies change. This checks whether the helper actually runs,
|
|
# not just whether the binary exists on disk.
|
|
check_aur_helper_functional() {
|
|
local helper="$1"
|
|
if $helper --version &>/dev/null; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# If yay is broken (common after Go or glibc updates), this rebuilds it
|
|
# from scratch by cloning the AUR package and running makepkg. This avoids
|
|
# a chicken-and-egg problem where you need an AUR helper to install an
|
|
# AUR helper.
|
|
rebuild_yay() {
|
|
info "Attempting to rebuild yay..."
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
pushd "$tmp_dir" >/dev/null || return 1
|
|
if git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm; then
|
|
popd >/dev/null || true
|
|
rm -rf "$tmp_dir"
|
|
info "yay rebuilt successfully"
|
|
return 0
|
|
else
|
|
popd >/dev/null || true
|
|
rm -rf "$tmp_dir"
|
|
err "Failed to rebuild yay"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Sanity check — make sure we're actually running on an Omarchy system.
|
|
# pacman = Arch Linux, hyprctl = Hyprland compositor, ~/.config/hypr = config dir.
|
|
# If any of these are missing, the script can't do its job.
|
|
validate_environment() {
|
|
command -v pacman >/dev/null || die "pacman required"
|
|
command -v hyprctl >/dev/null || die "hyprctl required"
|
|
[ -d "$HOME/.config/hypr" ] || die "Hyprland config directory not found (~/.config/hypr)"
|
|
}
|
|
|
|
# Quick check if a pacman package is installed. Used throughout the script
|
|
# to avoid reinstalling things that are already present.
|
|
check_package() { pacman -Qi "$1" &>/dev/null; }
|
|
|
|
# Distinguishes AMD integrated GPUs (iGPUs/APUs) from discrete AMD GPUs (dGPUs).
|
|
# This matters because Gaming Mode needs to target the RIGHT GPU — if you have
|
|
# both an AMD APU and an NVIDIA dGPU, we want to use the NVIDIA card for gaming,
|
|
# not the low-power APU that's driving your laptop display.
|
|
#
|
|
# It works by reading the PCI device info and matching against known AMD APU
|
|
# codenames (Phoenix, Rembrandt, Van Gogh, etc.). If the name matches an APU
|
|
# pattern, it returns 0 (true = this IS an iGPU). If it matches a discrete
|
|
# GPU pattern (Navi, RX, Vega 56/64), it returns 1 (false = this is a dGPU).
|
|
is_amd_igpu_card() {
|
|
local card_path="$1"
|
|
local device_path="$card_path/device"
|
|
local pci_slot=""
|
|
[[ -L "$device_path" ]] && pci_slot=$(basename "$(readlink -f "$device_path")")
|
|
[[ -z "$pci_slot" ]] && return 1
|
|
local device_info=$(/usr/bin/lspci -s "$pci_slot" 2>/dev/null)
|
|
if echo "$device_info" | grep -iqE 'renoir|cezanne|barcelo|rembrandt|phoenix|raphael|lucienne|picasso|raven|vega.*mobile|vega.*integrated|radeon.*graphics|yellow.*carp|green.*sardine|cyan.*skillfish|vangogh|van gogh|mendocino|hawk.*point|strix.*point|strix.*halo|krackan|sarlak'; then
|
|
return 0
|
|
fi
|
|
if echo "$device_info" | grep -iqE 'radeon rx|navi [0-9]|navi[0-9]|vega 56|vega 64|radeon vii|radeon pro|firepro|polaris|ellesmere|baffin|lexa|radeon [0-9]{3,4}[^0-9]'; then
|
|
return 1
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Intel-only GPU detection — Gaming Mode doesn't support Intel GPUs because
|
|
# Gamescope (the compositor) doesn't work well with Intel graphics.
|
|
# However, many laptops have Intel iGPU + NVIDIA/AMD dGPU — those are fine
|
|
# because we use the discrete GPU for gaming. This function only blocks
|
|
# systems where Intel is the ONLY GPU available.
|
|
check_intel_only() {
|
|
# Returns 0 (true) if system has Intel GPU but NO AMD/NVIDIA GPU
|
|
# Returns 1 (false) if system has AMD/NVIDIA (Intel iGPU + dGPU is OK)
|
|
local card_name driver driver_link
|
|
local has_intel=false
|
|
local has_amd_nvidia=false
|
|
|
|
for card_path in /sys/class/drm/card[0-9]*; do
|
|
card_name=$(basename "$card_path")
|
|
[[ "$card_name" == render* ]] && continue
|
|
driver_link="$card_path/device/driver"
|
|
[[ -L "$driver_link" ]] || continue
|
|
driver=$(basename "$(readlink "$driver_link")")
|
|
|
|
case "$driver" in
|
|
i915|xe)
|
|
has_intel=true
|
|
;;
|
|
nvidia|amdgpu)
|
|
has_amd_nvidia=true
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Block only if Intel exists and no AMD/NVIDIA exists
|
|
if $has_intel && ! $has_amd_nvidia; then
|
|
return 0 # Intel-only, block it
|
|
fi
|
|
return 1 # Has AMD/NVIDIA (or no Intel), allow it
|
|
}
|
|
|
|
# Finds which monitors are physically connected to the discrete GPU.
|
|
# Gaming Mode needs to know which display output to use (e.g. HDMI-1, DP-2)
|
|
# and what resolution/refresh rate it supports.
|
|
#
|
|
# It scans /sys/class/drm/ for GPU cards, identifies the discrete GPU by its
|
|
# driver (nvidia or amdgpu), then checks each connector (HDMI, DP, etc.) to
|
|
# see if a monitor is plugged in. Resolution and refresh rate are read from
|
|
# the EDID data that the monitor reports to the system.
|
|
#
|
|
# Uses bash nameref variables (_monitors, _dgpu_card, _dgpu_type) so the
|
|
# caller gets the results without needing subshells or global variables.
|
|
detect_dgpu_monitors() {
|
|
local -n _monitors=$1
|
|
local -n _dgpu_card=$2
|
|
local -n _dgpu_type=$3
|
|
_monitors=()
|
|
_dgpu_card=""
|
|
_dgpu_type=""
|
|
|
|
local lspci_output
|
|
lspci_output=$(/usr/bin/lspci 2>/dev/null)
|
|
if echo "$lspci_output" | grep -qi nvidia; then
|
|
_dgpu_type="NVIDIA"
|
|
elif echo "$lspci_output" | grep -iqE 'radeon rx|navi|vega 56|vega 64|radeon vii|radeon pro'; then
|
|
_dgpu_type="AMD dGPU"
|
|
fi
|
|
|
|
for card_path in /sys/class/drm/card[0-9]*; do
|
|
local card_name=$(basename "$card_path")
|
|
[[ "$card_name" == render* ]] && continue
|
|
local driver_link="$card_path/device/driver"
|
|
[[ -L "$driver_link" ]] || continue
|
|
local driver=$(basename "$(readlink "$driver_link")")
|
|
local is_dgpu=false
|
|
|
|
case "$driver" in
|
|
nvidia)
|
|
is_dgpu=true
|
|
[[ -z "$_dgpu_type" ]] && _dgpu_type="NVIDIA"
|
|
;;
|
|
amdgpu)
|
|
if ! is_amd_igpu_card "$card_path"; then
|
|
is_dgpu=true
|
|
[[ -z "$_dgpu_type" ]] && _dgpu_type="AMD dGPU"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if $is_dgpu; then
|
|
_dgpu_card="$card_name"
|
|
for connector in "$card_path"/"$card_name"-*/status; do
|
|
[[ -f "$connector" ]] || continue
|
|
local conn_dir=$(dirname "$connector")
|
|
local conn_name=$(basename "$conn_dir")
|
|
conn_name=${conn_name#card*-}
|
|
[[ "$conn_name" == Writeback* ]] && continue
|
|
local status=$(cat "$connector" 2>/dev/null)
|
|
if [[ "$status" == "connected" ]]; then
|
|
local resolution=""
|
|
local mode_file="$conn_dir/modes"
|
|
[[ -f "$mode_file" ]] && [[ -s "$mode_file" ]] && resolution=$(head -1 "$mode_file" 2>/dev/null)
|
|
_monitors+=("$conn_name|$resolution")
|
|
fi
|
|
done
|
|
break
|
|
fi
|
|
done
|
|
}
|
|
|
|
# NVIDIA GPUs require a kernel parameter (nvidia-drm.modeset=1) to work
|
|
# properly with Wayland compositors like Gamescope. Without it, the GPU
|
|
# won't expose DRM (Direct Rendering Manager) devices, and Gamescope
|
|
# won't be able to take over the display.
|
|
#
|
|
# This function checks if the parameter is already set in the running
|
|
# kernel's command line (/proc/cmdline). If not, it detects the bootloader
|
|
# (Limine, GRUB, or systemd-boot) and offers to add it automatically.
|
|
# A reboot is required after adding the parameter.
|
|
check_nvidia_kernel_params() {
|
|
local lspci_output
|
|
lspci_output=$(/usr/bin/lspci 2>/dev/null)
|
|
if ! echo "$lspci_output" | grep -qi nvidia; then
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " NVIDIA KERNEL PARAMETER CHECK"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
if grep -qE "nvidia[-_]drm\.modeset=1" /proc/cmdline 2>/dev/null; then
|
|
info "nvidia-drm.modeset=1 is already configured"
|
|
return 0
|
|
fi
|
|
|
|
warn "nvidia-drm.modeset=1 is NOT SET - required for Gaming Mode!"
|
|
echo ""
|
|
|
|
local bootloader=""
|
|
local config_file=""
|
|
|
|
if [ -f /boot/limine.conf ]; then
|
|
bootloader="limine"; config_file="/boot/limine.conf"
|
|
elif [ -f /boot/limine/limine.conf ]; then
|
|
bootloader="limine"; config_file="/boot/limine/limine.conf"
|
|
elif [ -d /boot/loader/entries ]; then
|
|
bootloader="systemd-boot"
|
|
elif [ -f /etc/default/grub ]; then
|
|
bootloader="grub"
|
|
fi
|
|
|
|
info "Detected bootloader: $bootloader"
|
|
|
|
case "$bootloader" in
|
|
limine)
|
|
info "Limine config: $config_file"
|
|
read -p "Add nvidia-drm.modeset=1 to Limine config? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
configure_limine_nvidia "$config_file"
|
|
else
|
|
warn "Skipping - you'll need to add nvidia-drm.modeset=1 manually"
|
|
show_manual_nvidia_instructions
|
|
fi
|
|
;;
|
|
systemd-boot)
|
|
echo ""
|
|
echo " systemd-boot detected. You need to add nvidia-drm.modeset=1"
|
|
echo " to your boot entry in /boot/loader/entries/*.conf"
|
|
echo ""
|
|
show_manual_nvidia_instructions
|
|
;;
|
|
grub)
|
|
echo ""
|
|
read -p "Add nvidia-drm.modeset=1 to GRUB config? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
configure_grub_nvidia
|
|
else
|
|
warn "Skipping - you'll need to add nvidia-drm.modeset=1 manually"
|
|
show_manual_nvidia_instructions
|
|
fi
|
|
;;
|
|
*)
|
|
echo ""
|
|
warn "Could not detect bootloader type"
|
|
show_manual_nvidia_instructions
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Adds nvidia-drm.modeset=1 to the Limine bootloader config.
|
|
# Limine stores kernel command line parameters on lines starting with "cmdline:".
|
|
# This appends the parameter to the end of that line. A backup is created first
|
|
# in case something goes wrong.
|
|
configure_limine_nvidia() {
|
|
local config_file="$1"
|
|
|
|
info "Backing up Limine config..."
|
|
sudo cp "$config_file" "${config_file}.backup.$(date +%Y%m%d%H%M%S)" || {
|
|
err "Failed to backup Limine config"
|
|
return 1
|
|
}
|
|
|
|
info "Adding nvidia-drm.modeset=1 to Limine cmdline..."
|
|
|
|
if sudo sed -i '/^[[:space:]]*cmdline:/ s/$/ nvidia-drm.modeset=1/' "$config_file"; then
|
|
if grep -q "nvidia-drm.modeset=1" "$config_file"; then
|
|
info "Successfully added nvidia-drm.modeset=1 to Limine config"
|
|
echo ""
|
|
echo " ✓ Limine config updated"
|
|
echo " ✓ Changes will take effect after reboot"
|
|
echo ""
|
|
NEEDS_REBOOT=1
|
|
else
|
|
err "Failed to add parameter - please add manually"
|
|
show_manual_nvidia_instructions
|
|
fi
|
|
else
|
|
err "Failed to modify Limine config"
|
|
show_manual_nvidia_instructions
|
|
fi
|
|
}
|
|
|
|
# Adds nvidia-drm.modeset=1 to GRUB's kernel parameters.
|
|
# GRUB stores defaults in /etc/default/grub — after modifying it, grub-mkconfig
|
|
# must be run to regenerate the actual boot config at /boot/grub/grub.cfg.
|
|
configure_grub_nvidia() {
|
|
local grub_default="/etc/default/grub"
|
|
|
|
info "Backing up GRUB config..."
|
|
sudo cp "$grub_default" "${grub_default}.backup.$(date +%Y%m%d%H%M%S)" || {
|
|
err "Failed to backup GRUB config"
|
|
return 1
|
|
}
|
|
|
|
info "Adding nvidia-drm.modeset=1 to GRUB..."
|
|
|
|
if ! grep -q "nvidia-drm.modeset=1" "$grub_default"; then
|
|
sudo sed -i 's/\(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*\)/\1 nvidia-drm.modeset=1/' "$grub_default"
|
|
|
|
if grep -q "nvidia-drm.modeset=1" "$grub_default"; then
|
|
info "Regenerating GRUB config..."
|
|
sudo grub-mkconfig -o /boot/grub/grub.cfg || {
|
|
err "Failed to regenerate GRUB config"
|
|
return 1
|
|
}
|
|
info "Successfully configured GRUB for NVIDIA"
|
|
NEEDS_REBOOT=1
|
|
else
|
|
err "Failed to add parameter to GRUB"
|
|
show_manual_nvidia_instructions
|
|
fi
|
|
fi
|
|
}
|
|
|
|
show_manual_nvidia_instructions() {
|
|
cat <<'MSG'
|
|
Manual configuration required:
|
|
Limine: Add nvidia-drm.modeset=1 to cmdline in /boot/limine.conf
|
|
systemd-boot: Add to options in /boot/loader/entries/*.conf
|
|
GRUB: Add to GRUB_CMDLINE_LINUX_DEFAULT, then run grub-mkconfig -o /boot/grub/grub.cfg
|
|
MSG
|
|
warn "Gaming Mode may not work correctly without nvidia-drm.modeset=1"
|
|
}
|
|
|
|
# Sets NVIDIA-specific environment variables needed for Gamescope to work
|
|
# properly with NVIDIA GPUs. These tell the system to use the NVIDIA DRM
|
|
# backend for GBM (Generic Buffer Management), which is how Wayland
|
|
# compositors allocate GPU memory for rendering.
|
|
#
|
|
# GBM_BACKEND=nvidia-drm — Use NVIDIA's DRM backend for buffer allocation
|
|
# __GLX_VENDOR_LIBRARY_NAME=nvidia — Use NVIDIA's GLX implementation
|
|
# __VK_LAYER_NV_optimus=NVIDIA_only — On Optimus laptops, force Vulkan to use NVIDIA
|
|
install_nvidia_deckmode_env() {
|
|
local lspci_output
|
|
lspci_output=$(/usr/bin/lspci 2>/dev/null)
|
|
if ! echo "$lspci_output" | grep -qi nvidia; then
|
|
info "No NVIDIA detected; skipping NVIDIA Deck-mode env."
|
|
return 0
|
|
fi
|
|
|
|
local env_file="/etc/environment.d/90-nvidia-gamescope.conf"
|
|
|
|
if [ -f "$env_file" ]; then
|
|
info "NVIDIA gamescope env already present: $env_file"
|
|
return 0
|
|
fi
|
|
|
|
info "Installing NVIDIA gamescope env (Deck-mode style)..."
|
|
sudo mkdir -p /etc/environment.d
|
|
|
|
sudo tee "$env_file" >/dev/null <<'EOF'
|
|
GBM_BACKEND=nvidia-drm
|
|
__GLX_VENDOR_LIBRARY_NAME=nvidia
|
|
__VK_LAYER_NV_optimus=NVIDIA_only
|
|
EOF
|
|
|
|
info "Installed $env_file"
|
|
NEEDS_RELOGIN=1
|
|
}
|
|
|
|
# Steam on Linux requires a LOT of dependencies — 32-bit libraries (lib32-*),
|
|
# Vulkan drivers, audio libraries, fonts, and GPU-specific drivers. This
|
|
# function checks for everything Steam needs and offers to install what's missing.
|
|
#
|
|
# It handles three categories:
|
|
# 1. Core deps — required for Steam to run at all (lib32 libs, Vulkan, audio)
|
|
# 2. GPU deps — driver packages specific to NVIDIA or AMD
|
|
# 3. Recommended — nice-to-haves like MangoHud (FPS overlay), Proton-GE, etc.
|
|
#
|
|
# The multilib repository must be enabled in pacman.conf for 32-bit packages
|
|
# to be available — Steam is a 32-bit application that needs 32-bit libraries.
|
|
check_steam_dependencies() {
|
|
info "Checking Steam dependencies for Arch Linux..."
|
|
|
|
info "Force refreshing package database from all mirrors..."
|
|
sudo pacman -Syy || die "Failed to refresh package database"
|
|
|
|
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 ""
|
|
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"
|
|
fi
|
|
echo ""
|
|
|
|
local -a missing_deps=()
|
|
local -a optional_deps=()
|
|
local multilib_enabled=false
|
|
|
|
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
|
|
|
|
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
|
|
|
|
local -a core_deps=(
|
|
"steam"
|
|
"lib32-vulkan-icd-loader"
|
|
"vulkan-icd-loader"
|
|
"lib32-mesa"
|
|
"mesa"
|
|
"mesa-utils"
|
|
"lib32-glibc"
|
|
"lib32-gcc-libs"
|
|
"lib32-libx11"
|
|
"lib32-libxss"
|
|
"lib32-alsa-plugins"
|
|
"lib32-libpulse"
|
|
"lib32-openal"
|
|
"lib32-nss"
|
|
"lib32-libcups"
|
|
"lib32-sdl2-compat"
|
|
"lib32-freetype2"
|
|
"lib32-fontconfig"
|
|
"lib32-libnm"
|
|
"networkmanager"
|
|
"gamemode"
|
|
"lib32-gamemode"
|
|
"ttf-liberation"
|
|
"xdg-user-dirs"
|
|
"kbd"
|
|
)
|
|
|
|
local gpu_vendor
|
|
gpu_vendor=$(/usr/bin/lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "")
|
|
|
|
local has_nvidia=false has_amd=false
|
|
|
|
if echo "$gpu_vendor" | grep -qi 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
|
|
info "Detected Intel GPU; no Intel-specific drivers will be installed"
|
|
fi
|
|
|
|
local primary_gpu="unknown"
|
|
if $has_nvidia; then
|
|
primary_gpu="nvidia"
|
|
elif $has_amd; then
|
|
primary_gpu="amd"
|
|
fi
|
|
|
|
PRIMARY_GPU="$primary_gpu"
|
|
info "Primary GPU selection: $PRIMARY_GPU"
|
|
|
|
local -a gpu_deps=()
|
|
|
|
if $has_nvidia; then
|
|
gpu_deps+=(
|
|
"nvidia-utils"
|
|
"lib32-nvidia-utils"
|
|
"nvidia-settings"
|
|
"libva-nvidia-driver"
|
|
)
|
|
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
|
|
|
|
if $has_amd; then
|
|
gpu_deps+=(
|
|
"vulkan-radeon"
|
|
"lib32-vulkan-radeon"
|
|
"libvdpau"
|
|
"lib32-libvdpau"
|
|
)
|
|
! check_package "xf86-video-amdgpu" && optional_deps+=("xf86-video-amdgpu")
|
|
fi
|
|
|
|
if ! $has_nvidia && ! $has_amd; then
|
|
info "No NVIDIA/AMD GPU detected; installing AMD Vulkan drivers as fallback..."
|
|
gpu_deps+=("vulkan-radeon" "lib32-vulkan-radeon")
|
|
fi
|
|
|
|
gpu_deps+=(
|
|
"vulkan-tools"
|
|
"vulkan-mesa-layers"
|
|
)
|
|
|
|
local -a recommended_deps=(
|
|
"gamescope"
|
|
"mangohud"
|
|
"lib32-mangohud"
|
|
"proton-ge-custom-bin"
|
|
"udisks2"
|
|
)
|
|
|
|
info "Checking core Steam dependencies..."
|
|
for dep in "${core_deps[@]}"; do
|
|
if ! check_package "$dep"; then
|
|
missing_deps+=("$dep")
|
|
fi
|
|
done
|
|
|
|
info "Checking GPU-specific dependencies..."
|
|
for dep in "${gpu_deps[@]}"; do
|
|
if ! check_package "$dep"; then
|
|
missing_deps+=("$dep")
|
|
fi
|
|
done
|
|
|
|
info "Checking recommended dependencies..."
|
|
for dep in "${recommended_deps[@]}"; do
|
|
if ! check_package "$dep"; then
|
|
optional_deps+=("$dep")
|
|
fi
|
|
done
|
|
|
|
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 ""
|
|
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
|
|
|
|
local -a clean_missing=()
|
|
for item in "${missing_deps[@]}"; do
|
|
[[ -n "$item" && "$item" != "multilib-repository" ]] && clean_missing+=("$item")
|
|
done
|
|
missing_deps=("${clean_missing[@]+"${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
|
|
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 "Syncing package database before installing..."
|
|
sudo pacman -Sy || warn "Failed to sync package database"
|
|
|
|
info "Installing recommended packages..."
|
|
# Check which packages are available in repos vs need AUR
|
|
local -a failed_deps=()
|
|
local -a pacman_deps=()
|
|
for dep in "${optional_deps[@]}"; do
|
|
if pacman -Si "$dep" &>/dev/null; then
|
|
pacman_deps+=("$dep")
|
|
else
|
|
failed_deps+=("$dep")
|
|
fi
|
|
done
|
|
if ((${#pacman_deps[@]})); then
|
|
sudo pacman -S --needed --noconfirm "${pacman_deps[@]}" || warn "Some packages failed to install"
|
|
fi
|
|
|
|
# If some packages failed, try with an official Arch mirror as fallback
|
|
if ((${#failed_deps[@]})); then
|
|
local mirrorlist="/etc/pacman.d/mirrorlist"
|
|
local fallback_mirror="Server = https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"
|
|
local added_fallback=false
|
|
|
|
if ! grep -q "geo.mirror.pkgbuild.com" "$mirrorlist" 2>/dev/null; then
|
|
info "Some packages not found in current repos. Adding official Arch mirror..."
|
|
sudo sed -i "1i $fallback_mirror" "$mirrorlist"
|
|
sudo pacman -Syy || warn "Failed to sync with official mirror"
|
|
added_fallback=true
|
|
fi
|
|
|
|
# Retry failed packages with updated database
|
|
local -a aur_optional=()
|
|
local -a retry_pacman=()
|
|
for dep in "${failed_deps[@]}"; do
|
|
if pacman -Si "$dep" &>/dev/null; then
|
|
retry_pacman+=("$dep")
|
|
else
|
|
aur_optional+=("$dep")
|
|
fi
|
|
done
|
|
if ((${#retry_pacman[@]})); then
|
|
info "Found packages in official repos: ${retry_pacman[*]}"
|
|
sudo pacman -S --needed --noconfirm "${retry_pacman[@]}" || warn "Some packages failed to install from official repos"
|
|
fi
|
|
|
|
# Remove the fallback mirror to restore original mirrorlist
|
|
if $added_fallback; then
|
|
sudo sed -i '/geo\.mirror\.pkgbuild\.com/d' "$mirrorlist"
|
|
sudo pacman -Syy 2>/dev/null || true
|
|
info "Removed fallback mirror, restored original mirrorlist"
|
|
fi
|
|
else
|
|
local -a aur_optional=()
|
|
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 ""
|
|
|
|
local aur_helper_available=""
|
|
if command -v yay >/dev/null 2>&1; then
|
|
if check_aur_helper_functional yay; then
|
|
aur_helper_available="yay"
|
|
else
|
|
warn "yay is installed but broken (needs rebuild after system update)"
|
|
read -p "Rebuild yay now? [Y/n]: " -n 1 -r
|
|
echo
|
|
REPLY=${REPLY:-Y}
|
|
if [[ $REPLY =~ ^[Yy]$ ]] && rebuild_yay && check_aur_helper_functional yay; then
|
|
aur_helper_available="yay"
|
|
fi
|
|
fi
|
|
elif command -v paru >/dev/null 2>&1; then
|
|
if check_aur_helper_functional paru; then
|
|
aur_helper_available="paru"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$aur_helper_available" ]]; then
|
|
read -p "Install AUR packages with $aur_helper_available? [y/N]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
# Install AUR packages one at a time so a single failure doesn't block others
|
|
for dep in "${aur_optional[@]}"; do
|
|
info "Installing $dep..."
|
|
$aur_helper_available -S --needed --noconfirm "$dep" || warn "Failed to install $dep from AUR"
|
|
done
|
|
fi
|
|
else
|
|
info "No functional AUR helper found (yay/paru). Install manually if desired."
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
info "All recommended packages are already installed!"
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
|
|
check_steam_config
|
|
}
|
|
|
|
# Enables the multilib repository in /etc/pacman.conf. Multilib provides
|
|
# 32-bit versions of libraries (lib32-*) which Steam requires because it's
|
|
# a 32-bit application. By default on Arch, these lines are commented out.
|
|
# This function uncomments them and runs a full system upgrade so the new
|
|
# packages become available.
|
|
enable_multilib_repo() {
|
|
info "Enabling multilib repository..."
|
|
|
|
sudo cp /etc/pacman.conf "/etc/pacman.conf.backup.$(date +%Y%m%d%H%M%S)" || die "Failed to backup pacman.conf"
|
|
sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf || die "Failed to enable multilib"
|
|
|
|
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"
|
|
fi
|
|
else
|
|
die "Failed to enable multilib repository"
|
|
fi
|
|
}
|
|
|
|
# Checks that the user is in the right Linux groups for gaming:
|
|
# - video: Access to GPU hardware (required for rendering)
|
|
# - input: Access to controllers, gamepads, and keyboard input devices
|
|
# - wheel: Sudo/admin group, needed for NetworkManager control in gaming mode
|
|
#
|
|
# Also checks for some common performance tips like lowering vm.swappiness
|
|
# (reduces swap usage during gaming) and increasing the open file limit
|
|
# (needed for esync, a Steam/Proton feature that reduces CPU overhead).
|
|
check_steam_config() {
|
|
info "Checking Steam configuration..."
|
|
|
|
local missing_groups=()
|
|
|
|
if ! groups | grep -qw 'video'; then
|
|
missing_groups+=("video")
|
|
fi
|
|
|
|
if ! groups | grep -qw 'input'; then
|
|
missing_groups+=("input")
|
|
fi
|
|
|
|
if ! groups | grep -qw 'wheel'; then
|
|
missing_groups+=("wheel")
|
|
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" ;;
|
|
wheel) echo " - wheel - Required for NetworkManager control in gaming mode" ;;
|
|
esac
|
|
done
|
|
echo ""
|
|
echo " NOTE: After adding groups, you MUST log out and log back in"
|
|
echo ""
|
|
read -p "Add user to ${missing_groups[*]} group(s)? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
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"
|
|
fi
|
|
fi
|
|
else
|
|
info "User is in video, input, and wheel groups - permissions OK"
|
|
fi
|
|
|
|
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
|
|
|
|
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"
|
|
fi
|
|
fi
|
|
|
|
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"
|
|
fi
|
|
}
|
|
|
|
# Sets up the permissions needed for Gaming Mode to tune system performance
|
|
# WITHOUT requiring a password prompt. This creates three things:
|
|
#
|
|
# 1. UDEV RULES — Make CPU governor and GPU performance files writable by
|
|
# regular users. Normally these sysfs files are root-only, but udev rules
|
|
# can relax permissions when the devices are detected at boot.
|
|
#
|
|
# 2. SUDOERS RULES — Allow members of the "video" group to run specific
|
|
# sysctl commands (kernel scheduler tuning, VM settings, network buffers)
|
|
# and nvidia-smi commands without entering a password. These are narrowly
|
|
# scoped — only the exact commands needed, not blanket sudo access.
|
|
#
|
|
# 3. MEMLOCK LIMITS — Increase the memory lock limit to 2GB. Games using
|
|
# esync/fsync need to lock memory pages to avoid latency spikes.
|
|
#
|
|
# 4. PIPEWIRE CONFIG — Sets a lower audio quantum (buffer size) for reduced
|
|
# audio latency during gaming. Lower = less delay but more CPU usage.
|
|
setup_performance_permissions() {
|
|
local udev_rules_file="/etc/udev/rules.d/99-gaming-performance.rules"
|
|
local sudoers_file="/etc/sudoers.d/gaming-mode-sysctl"
|
|
local needs_setup=false
|
|
|
|
if [ ! -f "$udev_rules_file" ] || [ ! -f "$sudoers_file" ]; then
|
|
needs_setup=true
|
|
fi
|
|
|
|
if [ "$needs_setup" = false ]; then
|
|
info "Performance permissions already configured"
|
|
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 ""
|
|
read -p "Set up passwordless performance controls? [Y/n]: " -n 1 -r
|
|
echo
|
|
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
info "Skipping permissions setup"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$udev_rules_file" ]; then
|
|
info "Creating udev rules for CPU/GPU performance control..."
|
|
|
|
if sudo tee "$udev_rules_file" > /dev/null <<'UDEV_RULES'
|
|
KERNEL=="cpu[0-9]*", SUBSYSTEM=="cpu", ACTION=="add", RUN+="/bin/chmod 666 /sys/devices/system/cpu/%k/cpufreq/scaling_governor"
|
|
KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="amdgpu", ACTION=="add", RUN+="/bin/chmod 666 /sys/class/drm/%k/device/power_dpm_force_performance_level"
|
|
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"
|
|
UDEV_RULES
|
|
then
|
|
info "Udev rules created successfully"
|
|
sudo udevadm control --reload-rules || true
|
|
sudo udevadm trigger --subsystem-match=cpu --subsystem-match=drm || true
|
|
fi
|
|
fi
|
|
|
|
if [[ -f "$sudoers_file" ]]; then
|
|
info "Performance sudoers already exist at $sudoers_file"
|
|
else
|
|
info "Creating sudoers rule for Performance Mode sysctl tuning..."
|
|
|
|
local tee_output
|
|
tee_output=$(sudo tee "$sudoers_file" << 'SUDOERS_PERF' 2>&1
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_autogroup_enabled=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_migration_cost_ns=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_min_granularity_ns=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w kernel.sched_latency_ns=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.swappiness=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_ratio=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_background_ratio=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_writeback_centisecs=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w vm.dirty_expire_centisecs=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.inotify.max_user_watches=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.inotify.max_user_instances=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w fs.file-max=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w net.core.rmem_max=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/sysctl -w net.core.wmem_max=*
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/nvidia-smi -pm *
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/nvidia-smi -pl *
|
|
SUDOERS_PERF
|
|
)
|
|
local tee_exit=$?
|
|
|
|
if [[ $tee_exit -eq 0 ]]; then
|
|
sudo chmod 0440 "$sudoers_file"
|
|
info "Performance sudoers created successfully"
|
|
else
|
|
err "Failed to create performance sudoers file (exit code: $tee_exit)"
|
|
fi
|
|
fi
|
|
|
|
local memlock_file="/etc/security/limits.d/99-gaming-memlock.conf"
|
|
if [ ! -f "$memlock_file" ]; then
|
|
info "Creating memlock limits for gaming performance..."
|
|
if sudo tee "$memlock_file" > /dev/null << 'MEMLOCKCONF'
|
|
* soft memlock 2147484
|
|
* hard memlock 2147484
|
|
MEMLOCKCONF
|
|
then
|
|
info "Memlock limits configured (2GB)"
|
|
fi
|
|
fi
|
|
|
|
local pipewire_conf_dir="/etc/pipewire/pipewire.conf.d"
|
|
local pipewire_conf="$pipewire_conf_dir/10-gaming-latency.conf"
|
|
if [ ! -f "$pipewire_conf" ]; then
|
|
info "Creating PipeWire low-latency audio configuration..."
|
|
sudo mkdir -p "$pipewire_conf_dir"
|
|
if sudo tee "$pipewire_conf" > /dev/null << 'PIPEWIRECONF'
|
|
context.properties = {
|
|
default.clock.min-quantum = 256
|
|
}
|
|
PIPEWIRECONF
|
|
then
|
|
info "PipeWire gaming latency configured"
|
|
fi
|
|
fi
|
|
|
|
info "Performance permissions configured"
|
|
return 0
|
|
}
|
|
|
|
# Configures shader cache settings for better gaming performance.
|
|
# When a game runs for the first time, the GPU driver compiles shaders
|
|
# (small programs that run on the GPU). This causes stuttering because
|
|
# compilation takes time. By caching these compiled shaders to disk
|
|
# (up to 12GB), the stuttering only happens once — next time the shader
|
|
# loads instantly from cache.
|
|
#
|
|
# MESA_SHADER_CACHE — AMD/Intel open-source driver cache
|
|
# __GL_SHADER_DISK_CACHE — NVIDIA proprietary driver cache
|
|
# DXVK_STATE_CACHE — Proton/Wine DirectX-to-Vulkan translation cache
|
|
# RADV_PERFTEST=gpl — AMD Vulkan: enables graphics pipeline library for
|
|
# faster shader compilation
|
|
setup_shader_cache() {
|
|
local env_file="/etc/environment.d/99-shader-cache.conf"
|
|
|
|
if [ -f "$env_file" ]; then
|
|
info "Shader cache configuration already exists"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SHADER CACHE OPTIMIZATION"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Configuring shader cache sizes for better gaming performance."
|
|
echo " This reduces stuttering in games by caching compiled shaders."
|
|
echo ""
|
|
read -p "Configure shader cache optimization? [Y/n]: " -n 1 -r
|
|
echo
|
|
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
info "Skipping shader cache configuration"
|
|
return 0
|
|
fi
|
|
|
|
info "Creating shader cache configuration..."
|
|
sudo mkdir -p /etc/environment.d || { warn "Failed to create /etc/environment.d"; return 0; }
|
|
local tmp_shader
|
|
tmp_shader=$(mktemp) || { warn "Failed to create temp file"; return 0; }
|
|
|
|
cat > "$tmp_shader" << 'SHADERCACHE'
|
|
MESA_SHADER_CACHE_MAX_SIZE=12G
|
|
MESA_SHADER_CACHE_DISABLE_CLEANUP=1
|
|
RADV_PERFTEST=gpl
|
|
__GL_SHADER_DISK_CACHE=1
|
|
__GL_SHADER_DISK_CACHE_SIZE=12884901888
|
|
__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1
|
|
DXVK_STATE_CACHE=1
|
|
FCITX_NO_WAYLAND_DIAGNOSE=1
|
|
SHADERCACHE
|
|
|
|
if sudo cp "$tmp_shader" "$env_file"; then
|
|
rm -f "$tmp_shader"
|
|
sudo chmod 644 "$env_file"
|
|
info "Shader cache configured for all GPUs (AMD/NVIDIA + Proton)"
|
|
else
|
|
rm -f "$tmp_shader"
|
|
warn "Failed to create shader cache configuration"
|
|
fi
|
|
}
|
|
|
|
# Silences a harmless but annoying warning from fcitx5 (input method framework).
|
|
# fcitx5 complains about Wayland support on every login, even if you don't use
|
|
# it for input. This sets FCITX_NO_WAYLAND_DIAGNOSE=1 to suppress the warning
|
|
# in both Hyprland config and the user's environment.
|
|
setup_fcitx_silence() {
|
|
local env_dir="$HOME/.config/environment.d"
|
|
local env_file="$env_dir/90-fcitx-wayland.conf"
|
|
local hypr_conf="$HOME/.config/hypr/hyprland.conf"
|
|
|
|
if [[ -f "$hypr_conf" ]]; then
|
|
if ! grep -q "FCITX_NO_WAYLAND_DIAGNOSE" "$hypr_conf" 2>/dev/null; then
|
|
echo "" >> "$hypr_conf"
|
|
echo "# Silence fcitx5 Wayland diagnose warning (gaming-mode installer)" >> "$hypr_conf"
|
|
echo "env = FCITX_NO_WAYLAND_DIAGNOSE,1" >> "$hypr_conf"
|
|
info "Added FCITX_NO_WAYLAND_DIAGNOSE to Hyprland config"
|
|
NEEDS_RELOGIN=1
|
|
fi
|
|
fi
|
|
|
|
if [[ ! -f "$env_file" ]] || ! grep -q "FCITX_NO_WAYLAND_DIAGNOSE=1" "$env_file" 2>/dev/null; then
|
|
mkdir -p "$env_dir" || return 0
|
|
cat > "$env_file" <<'EOF'
|
|
FCITX_NO_WAYLAND_DIAGNOSE=1
|
|
EOF
|
|
info "Created fcitx Wayland silence config"
|
|
NEEDS_RELOGIN=1
|
|
fi
|
|
}
|
|
|
|
# Configures the Elephant app launcher (used in Omarchy) to launch desktop
|
|
# applications through uwsm-app. UWSM (Universal Wayland Session Manager)
|
|
# ensures apps are properly associated with the Wayland session, which
|
|
# prevents issues with apps losing track of their display server.
|
|
configure_elephant_launcher() {
|
|
local cfg="$HOME/.config/elephant/desktopapplications.toml"
|
|
if [[ ! -f "$cfg" ]]; then
|
|
return 0
|
|
fi
|
|
if ! command -v uwsm-app >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
if grep -q '^launch_prefix[[:space:]]*=[[:space:]]*"uwsm-app --"' "$cfg" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
if grep -q '^launch_prefix[[:space:]]*=' "$cfg" 2>/dev/null; then
|
|
sed -i 's|^launch_prefix[[:space:]]*=.*|launch_prefix = "uwsm-app --"|' "$cfg"
|
|
else
|
|
echo 'launch_prefix = "uwsm-app --"' >> "$cfg"
|
|
fi
|
|
info "Configured Elephant desktopapplications launch_prefix (uwsm-app)"
|
|
restart_elephant_walker
|
|
}
|
|
|
|
# Restarts the Elephant/Walker launcher service so config changes take
|
|
# effect immediately without requiring a logout.
|
|
restart_elephant_walker() {
|
|
if ! systemctl --user show-environment >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if command -v omarchy-restart-walker >/dev/null 2>&1; then
|
|
omarchy-restart-walker >/dev/null 2>&1 || true
|
|
return 0
|
|
fi
|
|
systemctl --user restart elephant.service >/dev/null 2>&1 || true
|
|
systemctl --user restart app-walker@autostart.service >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Installs the core packages that the Gaming Mode scripts themselves need
|
|
# (as opposed to Steam's dependencies which are handled separately).
|
|
# These include:
|
|
# - python-evdev: Reads raw keyboard input for the Super+Shift+R hotkey
|
|
# - libcap: Sets Linux capabilities on gamescope (cap_sys_nice for priority)
|
|
# - ntfs-3g: Mounts NTFS-formatted game drives (common for Windows dual-boot)
|
|
# - xcb-util-cursor: X11 cursor support needed by some Proton games
|
|
#
|
|
# After installing packages, it also runs the sub-setup functions for
|
|
# performance permissions, shader cache, and gamescope capabilities.
|
|
setup_requirements() {
|
|
local -a required_packages=("steam" "gamescope" "mangohud" "python" "python-evdev" "libcap" "gamemode" "curl" "pciutils" "ntfs-3g" "xcb-util-cursor")
|
|
local -a packages_to_install=()
|
|
for pkg in "${required_packages[@]}"; do
|
|
check_package "$pkg" || packages_to_install+=("$pkg")
|
|
done
|
|
|
|
if ((${#packages_to_install[@]})); then
|
|
info "The following packages are required: ${packages_to_install[*]}"
|
|
read -p "Install missing packages? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
sudo pacman -S --needed "${packages_to_install[@]}" || die "package install failed"
|
|
else
|
|
die "Required packages missing - cannot continue"
|
|
fi
|
|
else
|
|
info "All required packages present."
|
|
fi
|
|
|
|
setup_performance_permissions
|
|
setup_fcitx_silence
|
|
setup_shader_cache
|
|
configure_elephant_launcher
|
|
|
|
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
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " GAMESCOPE CAPABILITY REQUEST"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Performance mode requires granting cap_sys_nice to gamescope."
|
|
echo ""
|
|
read -p "Grant cap_sys_nice to gamescope? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
sudo setcap 'cap_sys_nice=eip' "$(command -v gamescope)" || warn "Failed to set capability"
|
|
info "Capability granted to gamescope"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================
|
|
# SESSION SWITCHING — The heart of the installer
|
|
#
|
|
# This is the biggest and most important function. It sets up everything needed
|
|
# to seamlessly switch between Desktop Mode (Hyprland) and Gaming Mode
|
|
# (Gamescope + Steam Big Picture).
|
|
#
|
|
# The switching mechanism works through SDDM (the display/login manager):
|
|
# 1. User presses Super+Shift+S in Hyprland
|
|
# 2. switch-to-gaming script updates SDDM config to point to Gaming session
|
|
# 3. SDDM restarts and auto-logs into the Gaming Mode session
|
|
# 4. gamescope-session-nm-wrapper starts performance tuning, NetworkManager,
|
|
# drive mounting, keybind monitor, then launches Gamescope + Steam
|
|
# 5. When done (Super+Shift+R or Steam > Exit to Desktop), the reverse happens
|
|
#
|
|
# This function creates ALL the scripts and config files needed for this flow:
|
|
# - Session wrapper (gamescope-session-nm-wrapper)
|
|
# - Switch scripts (switch-to-gaming, switch-to-desktop)
|
|
# - Keybind monitor (Python daemon using evdev for Super+Shift+R)
|
|
# - NetworkManager start/stop scripts (iwd <-> NM handoff)
|
|
# - Steam library auto-mount daemon
|
|
# - SDDM session entry and config
|
|
# - Polkit and sudoers rules for passwordless operation
|
|
# - Hyprland keybind for Super+Shift+S
|
|
#
|
|
# It also installs ChimeraOS's gamescope-session packages from AUR, which
|
|
# provide the base session framework that the Steam Deck uses.
|
|
# ==============================================================================
|
|
setup_session_switching() {
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SESSION SWITCHING SETUP (Hyprland <-> Gamescope)"
|
|
echo " Using ChimeraOS gamescope-session packages"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
# Intel-only check - bail out early (Intel iGPU + AMD/NVIDIA dGPU is OK)
|
|
if check_intel_only; then
|
|
echo ""
|
|
echo " ███╗ ██╗ ██████╗ ██████╗ ██╗ ██████╗███████╗"
|
|
echo " ████╗ ██║██╔═══██╗ ██╔══██╗██║██╔════╝██╔════╝"
|
|
echo " ██╔██╗ ██║██║ ██║ ██║ ██║██║██║ █████╗ "
|
|
echo " ██║╚██╗██║██║ ██║ ██║ ██║██║██║ ██╔══╝ "
|
|
echo " ██║ ╚████║╚██████╔╝ ██████╔╝██║╚██████╗███████╗"
|
|
echo " ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝"
|
|
echo ""
|
|
err "NO DICE CHICAGO - INTEL DETECTED"
|
|
echo ""
|
|
echo " This setup does not support Intel GPUs (iGPU or Arc)."
|
|
echo " Gaming Mode requires AMD or NVIDIA graphics."
|
|
echo ""
|
|
exit 1
|
|
fi
|
|
|
|
echo " This will:"
|
|
echo " - Install gamescope-session-git and gamescope-session-steam-git from AUR"
|
|
echo " - Configure Super+Shift+S to switch to Gaming Mode"
|
|
echo " - Configure Steam's 'Exit to Desktop' to return to Hyprland"
|
|
echo ""
|
|
read -p "Set up session switching? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
info "Skipping session switching setup"
|
|
return 0
|
|
fi
|
|
|
|
local current_user="${SUDO_USER:-$USER}"
|
|
local user_home
|
|
user_home=$(eval echo "~$current_user")
|
|
|
|
local monitor_width=1920
|
|
local monitor_height=1080
|
|
local monitor_refresh=60
|
|
local monitor_output=""
|
|
|
|
local -a dgpu_monitors=()
|
|
local dgpu_card=""
|
|
local dgpu_type=""
|
|
detect_dgpu_monitors dgpu_monitors dgpu_card dgpu_type
|
|
|
|
if [[ -z "$dgpu_card" ]]; then
|
|
if [[ "$dgpu_type" == "NVIDIA" ]]; then
|
|
err "NVIDIA GPU detected but no DRM card found!"
|
|
echo ""
|
|
echo " This usually means nvidia-drm.modeset=1 is not set."
|
|
echo " The installer will configure this - please complete the setup"
|
|
echo " and REBOOT before running this section again."
|
|
echo ""
|
|
NEEDS_REBOOT=1
|
|
return 1
|
|
fi
|
|
# No dGPU - check for APU
|
|
local apu_card=""
|
|
local apu_monitors=()
|
|
local card_name driver_link driver conn_dir conn_name status resolution mode_file
|
|
|
|
for card_path in /sys/class/drm/card[0-9]*; do
|
|
card_name=$(basename "$card_path")
|
|
[[ "$card_name" == render* ]] && continue
|
|
driver_link="$card_path/device/driver"
|
|
[[ -L "$driver_link" ]] || continue
|
|
driver=$(basename "$(readlink "$driver_link")")
|
|
|
|
if [[ "$driver" == "amdgpu" ]] && is_amd_igpu_card "$card_path"; then
|
|
apu_card="$card_name"
|
|
# Find monitors connected to APU
|
|
for connector in "$card_path"/"$card_name"-*/status; do
|
|
[[ -f "$connector" ]] || continue
|
|
conn_dir=$(dirname "$connector")
|
|
conn_name=$(basename "$conn_dir")
|
|
conn_name=${conn_name#card*-}
|
|
[[ "$conn_name" == Writeback* ]] && continue
|
|
status=$(cat "$connector" 2>/dev/null)
|
|
if [[ "$status" == "connected" ]]; then
|
|
resolution=""
|
|
mode_file="$conn_dir/modes"
|
|
[[ -f "$mode_file" ]] && [[ -s "$mode_file" ]] && resolution=$(head -1 "$mode_file" 2>/dev/null)
|
|
apu_monitors+=("$conn_name|$resolution")
|
|
fi
|
|
done
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -n "$apu_card" && ${#apu_monitors[@]} -gt 0 ]]; then
|
|
echo ""
|
|
info "No discrete GPU found, but detected AMD APU ($apu_card)"
|
|
echo ""
|
|
echo " This system has an AMD APU which can run Gaming Mode."
|
|
echo " Detected monitors: ${#apu_monitors[@]}"
|
|
echo ""
|
|
read -p " Set up Gaming Mode for APU? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
# Use APU as the gaming GPU
|
|
dgpu_card="$apu_card"
|
|
dgpu_type="AMD APU"
|
|
dgpu_monitors=("${apu_monitors[@]}")
|
|
info "Configuring Gaming Mode for AMD APU"
|
|
else
|
|
info "Skipping APU Gaming Mode setup"
|
|
return 0
|
|
fi
|
|
else
|
|
err "No discrete GPU (dGPU) or AMD APU found!"
|
|
echo " Gaming mode requires a supported GPU with a connected display."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
info "Found $dgpu_type on $dgpu_card"
|
|
|
|
if [[ ${#dgpu_monitors[@]} -eq 0 ]]; then
|
|
err "No monitors connected to dGPU!"
|
|
echo ""
|
|
echo " Gaming mode requires a monitor connected to the discrete GPU."
|
|
echo " Please connect an external monitor to your dGPU port (HDMI/DP/USB-C)"
|
|
echo " and re-run this installer."
|
|
echo ""
|
|
return 1
|
|
fi
|
|
|
|
if [[ ${#dgpu_monitors[@]} -eq 1 ]]; then
|
|
local entry="${dgpu_monitors[0]}"
|
|
monitor_output="${entry%%|*}"
|
|
local res="${entry##*|}"
|
|
if [[ -n "$res" ]]; then
|
|
monitor_width="${res%%x*}"
|
|
monitor_height="${res##*x}"
|
|
monitor_height="${monitor_height%%@*}"
|
|
[[ "$res" == *@* ]] && monitor_refresh="${res##*@}" && monitor_refresh="${monitor_refresh%%.*}"
|
|
fi
|
|
else
|
|
echo ""
|
|
echo " Multiple monitors connected to $dgpu_type:"
|
|
local i=1
|
|
for entry in "${dgpu_monitors[@]}"; do
|
|
local name="${entry%%|*}"
|
|
local res="${entry##*|}"
|
|
echo " $i) $name ${res:+($res)}"
|
|
((i++))
|
|
done
|
|
echo ""
|
|
read -p "Select monitor for Gaming Mode [1-${#dgpu_monitors[@]}]: " selection
|
|
if [[ ! "$selection" =~ ^[0-9]+$ ]] || ((selection < 1 || selection > ${#dgpu_monitors[@]})); then
|
|
selection=1
|
|
fi
|
|
local entry="${dgpu_monitors[$((selection-1))]}"
|
|
monitor_output="${entry%%|*}"
|
|
local res="${entry##*|}"
|
|
if [[ -n "$res" ]]; then
|
|
monitor_width="${res%%x*}"
|
|
monitor_height="${res##*x}"
|
|
monitor_height="${monitor_height%%@*}"
|
|
[[ "$res" == *@* ]] && monitor_refresh="${res##*@}" && monitor_refresh="${monitor_refresh%%.*}"
|
|
fi
|
|
fi
|
|
|
|
info "Selected dGPU display: ${monitor_output} (${monitor_width}x${monitor_height}@${monitor_refresh}Hz)"
|
|
|
|
info "Checking for old custom session files to clean up..."
|
|
|
|
local -a old_files=(
|
|
"/usr/bin/gamescope-session"
|
|
"/usr/share/wayland-sessions/gamescope-session.desktop"
|
|
"/usr/bin/jupiter-biosupdate"
|
|
"/usr/bin/steamos-update"
|
|
"/usr/bin/steamos-select-branch"
|
|
"/usr/bin/steamos-session-select"
|
|
)
|
|
|
|
local cleaned=false
|
|
for old_file in "${old_files[@]}"; do
|
|
if [[ -f "$old_file" ]]; then
|
|
info "Removing old file: $old_file"
|
|
sudo rm -f "$old_file" && cleaned=true
|
|
fi
|
|
done
|
|
|
|
if $cleaned; then
|
|
info "Old custom session files removed"
|
|
else
|
|
info "No old files to clean up"
|
|
fi
|
|
|
|
info "Checking for ChimeraOS gamescope-session packages..."
|
|
|
|
local -a aur_packages=()
|
|
local -a packages_to_remove=()
|
|
|
|
if ! check_package "gamescope-session-git" && ! check_package "gamescope-session"; then
|
|
aur_packages+=("gamescope-session-git")
|
|
fi
|
|
|
|
local steam_scripts_missing=false
|
|
local -a required_steam_scripts=(
|
|
"/usr/bin/steamos-session-select"
|
|
"/usr/bin/steamos-update"
|
|
"/usr/bin/jupiter-biosupdate"
|
|
"/usr/bin/steamos-select-branch"
|
|
)
|
|
|
|
for script in "${required_steam_scripts[@]}"; do
|
|
if [[ ! -f "$script" ]]; then
|
|
steam_scripts_missing=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if ! check_package "gamescope-session-steam-git"; then
|
|
if check_package "gamescope-session-steam"; then
|
|
warn "gamescope-session-steam (non-git) is installed but missing Steam compatibility scripts"
|
|
info "The -git version from ChimeraOS includes required scripts:"
|
|
info " - steamos-session-select, steamos-update, jupiter-biosupdate, steamos-select-branch"
|
|
packages_to_remove+=("gamescope-session-steam")
|
|
fi
|
|
aur_packages+=("gamescope-session-steam-git")
|
|
elif $steam_scripts_missing; then
|
|
warn "gamescope-session-steam-git is installed but Steam compatibility scripts are missing!"
|
|
info "Will reinstall package to restore missing files:"
|
|
for script in "${required_steam_scripts[@]}"; do
|
|
if [[ ! -f "$script" ]]; then
|
|
info " - Missing: $script"
|
|
fi
|
|
done
|
|
packages_to_remove+=("gamescope-session-steam-git")
|
|
aur_packages+=("gamescope-session-steam-git")
|
|
fi
|
|
|
|
if ((${#aur_packages[@]})); then
|
|
echo ""
|
|
echo " The following AUR packages are required for ChimeraOS session:"
|
|
for pkg in "${aur_packages[@]}"; do
|
|
echo " - $pkg"
|
|
done
|
|
if ((${#packages_to_remove[@]})); then
|
|
echo ""
|
|
echo " The following packages need to be replaced:"
|
|
for pkg in "${packages_to_remove[@]}"; do
|
|
echo " - $pkg (will be removed)"
|
|
done
|
|
fi
|
|
echo ""
|
|
|
|
local aur_helper=""
|
|
if command -v yay >/dev/null 2>&1 && check_aur_helper_functional yay; then
|
|
aur_helper="yay"
|
|
elif command -v paru >/dev/null 2>&1 && check_aur_helper_functional paru; then
|
|
aur_helper="paru"
|
|
fi
|
|
|
|
if [[ -n "$aur_helper" ]]; then
|
|
read -p "Install ChimeraOS session packages with $aur_helper? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
if ((${#packages_to_remove[@]})); then
|
|
info "Removing conflicting packages: ${packages_to_remove[*]}"
|
|
sudo pacman -Rns --noconfirm "${packages_to_remove[@]}" || {
|
|
warn "Failed to remove old packages, trying to continue anyway..."
|
|
}
|
|
fi
|
|
|
|
info "Installing ChimeraOS gamescope-session packages..."
|
|
$aur_helper -S --needed --noconfirm --answeredit None --answerclean None --answerdiff None "${aur_packages[@]}" || {
|
|
err "Failed to install gamescope-session packages"
|
|
warn "You may need to install them manually: $aur_helper -S ${aur_packages[*]}"
|
|
}
|
|
fi
|
|
else
|
|
warn "No AUR helper found (yay/paru). Please install manually:"
|
|
if ((${#packages_to_remove[@]})); then
|
|
echo " sudo pacman -Rns ${packages_to_remove[*]}"
|
|
fi
|
|
echo " yay -S ${aur_packages[*]}"
|
|
echo ""
|
|
read -p "Press Enter to continue after installing, or Ctrl+C to abort..."
|
|
fi
|
|
else
|
|
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
|
|
fi
|
|
|
|
# NetworkManager Integration
|
|
#
|
|
# Omarchy uses iwd (Intel Wireless Daemon) for WiFi, but Steam requires
|
|
# NetworkManager for its network settings UI. These can't run simultaneously
|
|
# without conflicts, so we create a managed handoff:
|
|
# - On gaming session start: NM starts, takes over networking from iwd
|
|
# - On gaming session exit: NM stops, iwd restarts and reconnects to WiFi
|
|
#
|
|
# If iwd is active, we configure NM to use iwd as its WiFi backend so they
|
|
# cooperate instead of fighting. If systemd-networkd is also running, we
|
|
# tell NM to leave ethernet interfaces alone to avoid conflicts.
|
|
info "Setting up NetworkManager integration..."
|
|
if systemctl is-active --quiet iwd; then
|
|
info "Detected iwd is active - configuring NetworkManager to use iwd backend..."
|
|
sudo mkdir -p /etc/NetworkManager/conf.d
|
|
sudo tee /etc/NetworkManager/conf.d/10-iwd-backend.conf > /dev/null << 'NM_IWD_CONF'
|
|
[device]
|
|
wifi.backend=iwd
|
|
wifi.scan-rand-mac-address=no
|
|
|
|
[main]
|
|
plugins=ifupdown,keyfile
|
|
|
|
[ifupdown]
|
|
managed=false
|
|
|
|
[connection]
|
|
connection.autoconnect-slaves=0
|
|
NM_IWD_CONF
|
|
info "Created NetworkManager iwd backend configuration"
|
|
fi
|
|
|
|
if systemctl is-active --quiet systemd-networkd; then
|
|
info "Detected systemd-networkd - configuring NetworkManager to avoid conflicts..."
|
|
sudo tee /etc/NetworkManager/conf.d/20-unmanaged-systemd.conf > /dev/null << 'NM_UNMANAGED'
|
|
[keyfile]
|
|
unmanaged-devices=interface-name:en*;interface-name:eth*
|
|
NM_UNMANAGED
|
|
info "Configured NetworkManager to not manage ethernet interfaces"
|
|
fi
|
|
|
|
local nm_start_script="/usr/local/bin/gamescope-nm-start"
|
|
sudo tee "$nm_start_script" > /dev/null << 'NM_START'
|
|
#!/bin/bash
|
|
NM_MARKER="/tmp/.gamescope-started-nm"
|
|
LOG_TAG="gamescope-nm"
|
|
|
|
log() { logger -t "$LOG_TAG" "$*"; echo "$*"; }
|
|
|
|
if ! systemctl is-active --quiet NetworkManager.service; then
|
|
log "Starting NetworkManager service..."
|
|
systemctl start NetworkManager.service
|
|
if [ $? -eq 0 ]; then
|
|
touch "$NM_MARKER"
|
|
log "NetworkManager started successfully"
|
|
else
|
|
log "ERROR: Failed to start NetworkManager"
|
|
exit 1
|
|
fi
|
|
log "Waiting for NetworkManager to initialize..."
|
|
for i in {1..20}; do
|
|
if nmcli general status &>/dev/null; then
|
|
log "NetworkManager ready after ${i} attempts"
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
if nmcli general status 2>/dev/null | grep -q "connected"; then
|
|
log "Network connected and ready"
|
|
else
|
|
log "WARNING: NetworkManager running but not connected"
|
|
fi
|
|
else
|
|
log "NetworkManager already running"
|
|
fi
|
|
nmcli general status 2>/dev/null || log "WARNING: nmcli status check failed"
|
|
NM_START
|
|
sudo chmod +x "$nm_start_script"
|
|
|
|
local nm_stop_script="/usr/local/bin/gamescope-nm-stop"
|
|
sudo tee "$nm_stop_script" > /dev/null << 'NM_STOP'
|
|
#!/bin/bash
|
|
NM_MARKER="/tmp/.gamescope-started-nm"
|
|
LOG_TAG="gamescope-nm"
|
|
log() { logger -t "$LOG_TAG" "$*"; echo "$*"; }
|
|
|
|
if [ -f "$NM_MARKER" ]; then
|
|
rm -f "$NM_MARKER"
|
|
if systemctl is-active --quiet NetworkManager.service; then
|
|
log "Stopping NetworkManager service..."
|
|
systemctl stop NetworkManager.service 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
# Restart iwd to restore WiFi connection (must restart, not just start,
|
|
# because iwd may be "active" but not managing the interface while NM had control)
|
|
log "Restarting iwd to restore WiFi..."
|
|
systemctl restart iwd.service 2>/dev/null || true
|
|
sleep 3
|
|
|
|
# Find the wireless interface dynamically
|
|
WIFI_IFACE=$(iw dev 2>/dev/null | awk '/Interface/{print $2; exit}')
|
|
if [ -z "$WIFI_IFACE" ]; then
|
|
WIFI_IFACE=$(ls /sys/class/net/ 2>/dev/null | grep -E '^wl' | head -1)
|
|
fi
|
|
|
|
if [ -n "$WIFI_IFACE" ]; then
|
|
# Verify WiFi restoration (iwd auto-connects to known networks)
|
|
if iwctl station "$WIFI_IFACE" show 2>/dev/null | grep -qi "connected"; then
|
|
log "WiFi restored on $WIFI_IFACE"
|
|
else
|
|
log "Triggering WiFi scan on $WIFI_IFACE..."
|
|
iwctl station "$WIFI_IFACE" scan 2>/dev/null || true
|
|
sleep 3
|
|
if iwctl station "$WIFI_IFACE" show 2>/dev/null | grep -qi "connected"; then
|
|
log "WiFi connected on $WIFI_IFACE after scan"
|
|
else
|
|
log "WiFi on $WIFI_IFACE may need manual reconnection via iwctl"
|
|
fi
|
|
fi
|
|
else
|
|
log "No wireless interface found - skipping WiFi verification"
|
|
fi
|
|
else
|
|
log "No marker file found - NetworkManager was not started by gaming session"
|
|
fi
|
|
NM_STOP
|
|
sudo chmod +x "$nm_stop_script"
|
|
info "Created NetworkManager start/stop scripts"
|
|
|
|
# Steam Library Auto-Mount Daemon
|
|
#
|
|
# Many gamers have games spread across multiple drives (external SSDs,
|
|
# NTFS partitions from a Windows dual-boot, etc.). This daemon:
|
|
# 1. Scans all connected drives for Steam library folders (steamapps/)
|
|
# 2. Mounts drives that contain Steam libraries via udisks2
|
|
# 3. Unmounts drives that DON'T have Steam libraries (keeps it clean)
|
|
# 4. Watches for hot-plugged drives (USB drives plugged in during gaming)
|
|
#
|
|
# It runs in the background during Gaming Mode and stops when you exit.
|
|
local steam_mount_script="/usr/local/bin/steam-library-mount"
|
|
info "Creating Steam library drive mount script..."
|
|
sudo tee "$steam_mount_script" > /dev/null << 'STEAM_MOUNT'
|
|
#!/bin/bash
|
|
LOG_TAG="steam-library-mount"
|
|
MOUNT_BASE="/run/media/$USER"
|
|
|
|
log() { logger -t "$LOG_TAG" "$*"; }
|
|
|
|
check_steam_library() {
|
|
local mount_point="$1"
|
|
if [[ -d "$mount_point/steamapps" ]] || \
|
|
[[ -d "$mount_point/SteamLibrary/steamapps" ]] || \
|
|
[[ -d "$mount_point/SteamLibrary" ]] || \
|
|
[[ -f "$mount_point/libraryfolder.vdf" ]] || \
|
|
[[ -f "$mount_point/steamapps/libraryfolder.vdf" ]] || \
|
|
[[ -f "$mount_point/SteamLibrary/libraryfolder.vdf" ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
handle_device() {
|
|
local device="$1"
|
|
local part_name
|
|
part_name=$(basename "$device")
|
|
|
|
log "Checking device: $device"
|
|
|
|
if findmnt -n "$device" &>/dev/null; then
|
|
local existing_mount
|
|
existing_mount=$(findmnt -n -o TARGET "$device" 2>/dev/null)
|
|
if [[ -n "$existing_mount" ]] && check_steam_library "$existing_mount"; then
|
|
log "Steam library already mounted at $existing_mount"
|
|
else
|
|
log "Device $device mounted at $existing_mount (no Steam library)"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
[[ "$device" =~ [0-9]$ ]] || { log "Skipping whole disk: $device"; return; }
|
|
|
|
local fstype
|
|
fstype=$(lsblk -n -o FSTYPE --nodeps "$device" 2>/dev/null)
|
|
case "$fstype" in
|
|
ext4|ext3|ext2|btrfs|xfs|ntfs|vfat|exfat|f2fs) ;;
|
|
crypto_LUKS) log "Skipping encrypted: $device"; return ;;
|
|
swap) log "Skipping swap: $device"; return ;;
|
|
"") log "Skipping $device - no filesystem"; return ;;
|
|
*) log "Skipping $device - unsupported filesystem: $fstype"; return ;;
|
|
esac
|
|
|
|
if ! command -v udisksctl &>/dev/null; then
|
|
log "udisksctl not found - cannot mount $device"
|
|
return
|
|
fi
|
|
|
|
log "Attempting to mount $device..."
|
|
local mount_output
|
|
mount_output=$(udisksctl mount -b "$device" --no-user-interaction 2>&1)
|
|
local mount_rc=$?
|
|
|
|
if [[ $mount_rc -ne 0 ]]; then
|
|
log "Could not mount $device: $mount_output"
|
|
return
|
|
fi
|
|
|
|
local mount_point
|
|
mount_point=$(findmnt -n -o TARGET "$device" 2>/dev/null)
|
|
|
|
if [[ -z "$mount_point" ]]; then
|
|
log "Could not determine mount point for $device"
|
|
return
|
|
fi
|
|
|
|
if check_steam_library "$mount_point"; then
|
|
log "Steam library found on $device at $mount_point - keeping mounted"
|
|
else
|
|
log "No Steam library on $device - unmounting"
|
|
udisksctl unmount -b "$device" --no-user-interaction 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
log "Starting Steam library drive monitor..."
|
|
|
|
shopt -s nullglob
|
|
for dev in /dev/sd*[0-9]* /dev/nvme*p[0-9]*; do
|
|
[[ -b "$dev" ]] && handle_device "$dev"
|
|
done
|
|
shopt -u nullglob
|
|
|
|
log "Initial device scan complete, watching for new devices..."
|
|
|
|
udevadm monitor --kernel --subsystem-match=block 2>/dev/null | while read -r line; do
|
|
if [[ "$line" =~ ^KERNEL.*[[:space:]]add[[:space:]]+.*/([^/[:space:]]+)[[:space:]]+\(block\)$ ]]; then
|
|
dev_name="${BASH_REMATCH[1]}"
|
|
dev_path="/dev/$dev_name"
|
|
if [[ "$dev_name" =~ [0-9]$ ]] && [[ -b "$dev_path" ]]; then
|
|
sleep 1
|
|
handle_device "$dev_path"
|
|
fi
|
|
fi
|
|
done
|
|
STEAM_MOUNT
|
|
sudo chmod +x "$steam_mount_script"
|
|
info "Created $steam_mount_script"
|
|
|
|
# Polkit Rules
|
|
#
|
|
# Polkit is Linux's permission system for D-Bus actions. Steam communicates
|
|
# with NetworkManager over D-Bus, but by default only root can control NM.
|
|
# These rules allow members of the "wheel" group (admin users) to control
|
|
# NetworkManager without a password prompt — otherwise Steam would show
|
|
# a "permission denied" error when trying to access network settings.
|
|
local polkit_rules="/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"
|
|
|
|
if sudo test -f "$polkit_rules"; then
|
|
info "Polkit rules already exist at $polkit_rules"
|
|
else
|
|
info "Creating Polkit rules for NetworkManager D-Bus access..."
|
|
|
|
local polkit_output
|
|
polkit_output=$(sudo tee "$polkit_rules" << 'POLKIT_RULES' 2>&1
|
|
polkit.addRule(function(action, subject) {
|
|
if ((action.id == "org.freedesktop.NetworkManager.enable-disable-network" ||
|
|
action.id == "org.freedesktop.NetworkManager.enable-disable-wifi" ||
|
|
action.id == "org.freedesktop.NetworkManager.network-control" ||
|
|
action.id == "org.freedesktop.NetworkManager.wifi.scan" ||
|
|
action.id == "org.freedesktop.NetworkManager.settings.modify.system" ||
|
|
action.id == "org.freedesktop.NetworkManager.settings.modify.own" ||
|
|
action.id == "org.freedesktop.NetworkManager.settings.modify.hostname") &&
|
|
subject.isInGroup("wheel")) {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
POLKIT_RULES
|
|
)
|
|
local polkit_exit=$?
|
|
|
|
if [[ $polkit_exit -eq 0 ]]; then
|
|
sudo chmod 644 "$polkit_rules"
|
|
info "Polkit rules created successfully"
|
|
sudo systemctl restart polkit.service 2>/dev/null || true
|
|
else
|
|
err "Failed to create polkit rules file (exit code: $polkit_exit)"
|
|
fi
|
|
fi
|
|
|
|
# Udisks2 Polkit Rules — same concept as above but for drive mounting.
|
|
# Allows the steam-library-mount daemon to mount/unmount drives without
|
|
# a password prompt. Without this, plugging in a USB drive with games
|
|
# would show a password dialog — not ideal in the middle of a gaming session.
|
|
local udisks_polkit="/etc/polkit-1/rules.d/50-udisks-gaming.rules"
|
|
|
|
if sudo test -f "$udisks_polkit"; then
|
|
info "Udisks2 polkit rules already exist at $udisks_polkit"
|
|
else
|
|
info "Creating Polkit rules for external drive auto-mount..."
|
|
sudo mkdir -p /etc/polkit-1/rules.d
|
|
sudo tee "$udisks_polkit" > /dev/null << 'UDISKS_POLKIT'
|
|
polkit.addRule(function(action, subject) {
|
|
if ((action.id == "org.freedesktop.udisks2.filesystem-mount" ||
|
|
action.id == "org.freedesktop.udisks2.filesystem-mount-system" ||
|
|
action.id == "org.freedesktop.udisks2.filesystem-unmount-others" ||
|
|
action.id == "org.freedesktop.udisks2.encrypted-unlock" ||
|
|
action.id == "org.freedesktop.udisks2.power-off-drive") &&
|
|
subject.isInGroup("wheel")) {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
UDISKS_POLKIT
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
sudo chmod 644 "$udisks_polkit"
|
|
info "Udisks2 polkit rules created successfully"
|
|
sudo systemctl restart polkit.service 2>/dev/null || true
|
|
else
|
|
err "Failed to create udisks2 polkit rules"
|
|
fi
|
|
fi
|
|
|
|
# Gamescope Session Configuration
|
|
#
|
|
# This config file tells gamescope-session-plus (from ChimeraOS) how to
|
|
# set up the gaming display. It includes:
|
|
# - Resolution and refresh rate (auto-detected from your monitor)
|
|
# - Which display output to use (e.g. HDMI-1, DP-2)
|
|
# - GPU-specific settings (NVIDIA vs AMD have different requirements)
|
|
#
|
|
# NVIDIA gets: GBM_BACKEND=nvidia-drm, VULKAN_ADAPTER pointing to the GPU
|
|
# AMD gets: ADAPTIVE_SYNC=1 (FreeSync), ENABLE_GAMESCOPE_HDR=1 (HDR support)
|
|
#
|
|
# NVIDIA is capped at 2560x1440 due to Gamescope limitations with NVIDIA GPUs.
|
|
info "Creating gamescope-session-plus configuration..."
|
|
local env_dir="${user_home}/.config/environment.d"
|
|
local gamescope_conf="${env_dir}/gamescope-session-plus.conf"
|
|
|
|
mkdir -p "$env_dir"
|
|
|
|
local output_connector=""
|
|
[[ -n "$monitor_output" ]] && output_connector="OUTPUT_CONNECTOR=$monitor_output"
|
|
|
|
local is_nvidia=false
|
|
local nvidia_device_id=""
|
|
if [[ "$dgpu_type" == "NVIDIA" ]]; then
|
|
is_nvidia=true
|
|
nvidia_device_id=$(/usr/bin/lspci -nn | grep -i nvidia | grep -oP '\[10de:\K[0-9a-fA-F]+' | head -1)
|
|
if [ "$monitor_width" -gt 2560 ]; then
|
|
monitor_width=2560
|
|
fi
|
|
if [ "$monitor_height" -gt 1440 ]; then
|
|
monitor_height=1440
|
|
fi
|
|
fi
|
|
|
|
if $is_nvidia; then
|
|
local vulkan_adapter=""
|
|
[[ -n "$nvidia_device_id" ]] && vulkan_adapter="VULKAN_ADAPTER=10de:${nvidia_device_id}"
|
|
cat > "$gamescope_conf" << GAMESCOPE_CONF
|
|
SCREEN_WIDTH=${monitor_width}
|
|
SCREEN_HEIGHT=${monitor_height}
|
|
CUSTOM_REFRESH_RATES=${monitor_refresh}
|
|
${output_connector}
|
|
${vulkan_adapter}
|
|
GBM_BACKEND=nvidia-drm
|
|
STEAM_ALLOW_DRIVE_UNMOUNT=1
|
|
FCITX_NO_WAYLAND_DIAGNOSE=1
|
|
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
|
|
GAMESCOPE_CONF
|
|
else
|
|
cat > "$gamescope_conf" << GAMESCOPE_CONF
|
|
SCREEN_WIDTH=${monitor_width}
|
|
SCREEN_HEIGHT=${monitor_height}
|
|
CUSTOM_REFRESH_RATES=${monitor_refresh}
|
|
${output_connector}
|
|
ADAPTIVE_SYNC=1
|
|
ENABLE_GAMESCOPE_HDR=1
|
|
STEAM_ALLOW_DRIVE_UNMOUNT=1
|
|
FCITX_NO_WAYLAND_DIAGNOSE=1
|
|
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
|
|
GAMESCOPE_CONF
|
|
fi
|
|
|
|
info "Created $gamescope_conf"
|
|
|
|
# NVIDIA Gamescope Wrapper
|
|
#
|
|
# NVIDIA GPUs need the --force-composition flag in Gamescope to avoid
|
|
# rendering glitches. This wrapper script sits in front of the real
|
|
# gamescope binary — when Gaming Mode starts, it calls this wrapper
|
|
# instead, which adds the flag and then exec's the real gamescope.
|
|
# It checks if the flag is actually supported first (older versions don't have it).
|
|
info "Creating NVIDIA gamescope wrapper..."
|
|
local nvidia_wrapper_dir="/usr/local/lib/gamescope-nvidia"
|
|
local nvidia_wrapper="${nvidia_wrapper_dir}/gamescope"
|
|
|
|
sudo mkdir -p "$nvidia_wrapper_dir"
|
|
sudo tee "$nvidia_wrapper" > /dev/null << 'NVIDIA_WRAPPER'
|
|
#!/bin/bash
|
|
EXTRA_ARGS=""
|
|
if /usr/bin/gamescope --help 2>&1 | grep -q "force-composition"; then
|
|
EXTRA_ARGS="--force-composition"
|
|
fi
|
|
exec /usr/bin/gamescope $EXTRA_ARGS "$@"
|
|
NVIDIA_WRAPPER
|
|
|
|
sudo chmod +x "$nvidia_wrapper"
|
|
info "Created $nvidia_wrapper"
|
|
|
|
# Main Session Wrapper — gamescope-session-nm-wrapper
|
|
#
|
|
# This is the master script that runs when Gaming Mode starts. It's the
|
|
# entry point for the entire gaming session and orchestrates everything:
|
|
#
|
|
# 1. Enables performance mode (CPU governor → performance, GPU max power)
|
|
# 2. Adds NVIDIA wrapper to PATH if needed
|
|
# 3. Starts NetworkManager for Steam's network access
|
|
# 4. Launches steam-library-mount daemon for external drive detection
|
|
# 5. Starts the keybind monitor (listens for Super+Shift+R to exit)
|
|
# 6. Sets Steam-specific environment variables
|
|
# 7. Launches gamescope-session-plus (the actual Gamescope + Steam session)
|
|
#
|
|
# When the session ends (for any reason), the cleanup trap:
|
|
# - Kills the mount daemon and keybind monitor
|
|
# - Stops NetworkManager and restores iwd WiFi
|
|
# - Restores balanced power mode (CPU powersave, GPU defaults)
|
|
# - Removes the session marker file
|
|
info "Creating NetworkManager session wrapper..."
|
|
local nm_wrapper="/usr/local/bin/gamescope-session-nm-wrapper"
|
|
|
|
sudo tee "$nm_wrapper" > /dev/null << 'NM_WRAPPER'
|
|
#!/bin/bash
|
|
log() { logger -t gamescope-wrapper "$*"; echo "$*"; }
|
|
|
|
enable_performance_mode() {
|
|
log "Enabling performance mode..."
|
|
|
|
# Set CPU governor to performance
|
|
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
|
echo performance > "$gov" 2>/dev/null && log "CPU governor set to performance"
|
|
break # Log only once
|
|
done
|
|
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
|
echo performance > "$gov" 2>/dev/null
|
|
done
|
|
|
|
# NVIDIA dGPU performance mode
|
|
if command -v nvidia-smi &>/dev/null; then
|
|
# Enable persistence mode (keeps GPU initialized)
|
|
sudo -n nvidia-smi -pm 1 2>/dev/null && log "NVIDIA persistence mode enabled"
|
|
|
|
# Set power limit to maximum
|
|
local max_power
|
|
max_power=$(nvidia-smi --query-gpu=power.max_limit --format=csv,noheader,nounits 2>/dev/null | head -1 | cut -d'.' -f1)
|
|
if [[ -n "$max_power" && "$max_power" -gt 0 ]]; then
|
|
sudo -n nvidia-smi -pl "$max_power" 2>/dev/null && log "NVIDIA power limit set to ${max_power}W"
|
|
fi
|
|
|
|
# Prevent NVIDIA GPU runtime suspend
|
|
for nvidia_pci in /sys/bus/pci/devices/*/power/control; do
|
|
if [[ -f "${nvidia_pci%/power/control}/driver" ]]; then
|
|
local drv=$(basename "$(readlink -f "${nvidia_pci%/power/control}/driver")" 2>/dev/null)
|
|
if [[ "$drv" == "nvidia" ]]; then
|
|
echo on > "$nvidia_pci" 2>/dev/null && log "NVIDIA runtime suspend disabled"
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Set power profile to performance (if power-profiles-daemon is available)
|
|
if command -v powerprofilesctl &>/dev/null; then
|
|
powerprofilesctl set performance 2>/dev/null && log "Power profile set to performance"
|
|
fi
|
|
}
|
|
|
|
restore_balanced_mode() {
|
|
log "Restoring balanced mode..."
|
|
|
|
# Restore CPU governor to powersave/schedutil
|
|
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
|
echo powersave > "$gov" 2>/dev/null
|
|
done
|
|
|
|
# NVIDIA dGPU restore
|
|
if command -v nvidia-smi &>/dev/null; then
|
|
# Restore default power limit
|
|
local default_power
|
|
default_power=$(nvidia-smi --query-gpu=power.default_limit --format=csv,noheader,nounits 2>/dev/null | head -1 | cut -d'.' -f1)
|
|
if [[ -n "$default_power" && "$default_power" -gt 0 ]]; then
|
|
sudo -n nvidia-smi -pl "$default_power" 2>/dev/null
|
|
fi
|
|
|
|
# Re-enable NVIDIA GPU runtime suspend (power saving)
|
|
for nvidia_pci in /sys/bus/pci/devices/*/power/control; do
|
|
if [[ -f "${nvidia_pci%/power/control}/driver" ]]; then
|
|
local drv=$(basename "$(readlink -f "${nvidia_pci%/power/control}/driver")" 2>/dev/null)
|
|
if [[ "$drv" == "nvidia" ]]; then
|
|
echo auto > "$nvidia_pci" 2>/dev/null
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Disable persistence mode (allow GPU to sleep)
|
|
sudo -n nvidia-smi -pm 0 2>/dev/null
|
|
fi
|
|
|
|
# Restore power profile to balanced
|
|
if command -v powerprofilesctl &>/dev/null; then
|
|
powerprofilesctl set balanced 2>/dev/null
|
|
fi
|
|
|
|
log "Balanced mode restored"
|
|
}
|
|
|
|
cleanup() {
|
|
pkill -f steam-library-mount 2>/dev/null || true
|
|
pkill -f gaming-keybind-monitor 2>/dev/null || true
|
|
sudo -n /usr/local/bin/gamescope-nm-stop 2>/dev/null || true
|
|
restore_balanced_mode
|
|
rm -f /tmp/.gaming-session-active
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Enable performance mode immediately on session start
|
|
enable_performance_mode
|
|
|
|
if /usr/bin/lspci 2>/dev/null | grep -qi nvidia; then
|
|
export PATH="/usr/local/lib/gamescope-nvidia:$PATH"
|
|
fi
|
|
|
|
sudo -n /usr/local/bin/gamescope-nm-start 2>/dev/null || {
|
|
log "Warning: Could not start NetworkManager"
|
|
}
|
|
|
|
if [[ -x /usr/local/bin/steam-library-mount ]]; then
|
|
/usr/local/bin/steam-library-mount &
|
|
log "Steam library drive monitor started"
|
|
else
|
|
log "Warning: steam-library-mount not found"
|
|
fi
|
|
|
|
echo "gamescope" > /tmp/.gaming-session-active
|
|
|
|
keybind_ok=true
|
|
|
|
if ! python3 -c "import evdev" 2>/dev/null; then
|
|
log "WARNING: python-evdev not installed"
|
|
keybind_ok=false
|
|
fi
|
|
|
|
if ! groups | grep -qw input; then
|
|
log "WARNING: User not in 'input' group"
|
|
keybind_ok=false
|
|
fi
|
|
|
|
if $keybind_ok && ! ls /dev/input/event* >/dev/null 2>&1; then
|
|
log "WARNING: No input devices accessible"
|
|
keybind_ok=false
|
|
fi
|
|
|
|
if $keybind_ok; then
|
|
/usr/local/bin/gaming-keybind-monitor &
|
|
log "Keybind monitor started (Super+Shift+R to exit)"
|
|
else
|
|
log "Keybind monitor NOT started"
|
|
fi
|
|
|
|
export QT_IM_MODULE=steam
|
|
export GTK_IM_MODULE=Steam
|
|
export STEAM_DISABLE_AUDIO_DEVICE_SWITCHING=1
|
|
export STEAM_ENABLE_VOLUME_HANDLER=1
|
|
|
|
/usr/share/gamescope-session-plus/gamescope-session-plus steam
|
|
rc=$?
|
|
|
|
exit $rc
|
|
NM_WRAPPER
|
|
|
|
sudo chmod +x "$nm_wrapper"
|
|
info "Created $nm_wrapper"
|
|
|
|
# SDDM Session Entry
|
|
#
|
|
# SDDM (the login/display manager) needs a .desktop file to know about
|
|
# Gaming Mode as a session option. This is what tells SDDM "when you
|
|
# auto-login to the gaming session, run this script." It's placed in
|
|
# /usr/share/wayland-sessions/ alongside the normal Hyprland session.
|
|
info "Creating SDDM session entry..."
|
|
local session_desktop="/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"
|
|
|
|
sudo tee "$session_desktop" > /dev/null << 'SESSION_DESKTOP'
|
|
[Desktop Entry]
|
|
Name=Gaming Mode (ChimeraOS)
|
|
Comment=Steam Big Picture with ChimeraOS gamescope-session
|
|
Exec=/usr/local/bin/gamescope-session-nm-wrapper
|
|
Type=Application
|
|
DesktopNames=gamescope
|
|
SESSION_DESKTOP
|
|
|
|
info "Created $session_desktop"
|
|
|
|
# os-session-select — Steam's "Exit to Desktop" handler
|
|
#
|
|
# When you click Steam > Power > "Exit to Desktop" inside Gaming Mode,
|
|
# Steam calls /usr/lib/os-session-select. On a real Steam Deck this
|
|
# switches to Desktop Mode. Our version does the same thing — it updates
|
|
# SDDM to boot back into Hyprland and restarts the display manager.
|
|
info "Creating session-select script..."
|
|
local os_session_select="/usr/lib/os-session-select"
|
|
|
|
sudo tee "$os_session_select" > /dev/null << 'OS_SESSION_SELECT'
|
|
#!/bin/bash
|
|
rm -f /tmp/.gaming-session-active
|
|
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || {
|
|
echo "Warning: Failed to update session config"
|
|
}
|
|
timeout 5 steam -shutdown 2>/dev/null || true
|
|
sleep 1
|
|
nohup sudo -n systemctl restart sddm &>/dev/null &
|
|
disown
|
|
exit 0
|
|
OS_SESSION_SELECT
|
|
|
|
sudo chmod +x "$os_session_select"
|
|
info "Created $os_session_select"
|
|
|
|
# switch-to-gaming — Called when Super+Shift+S is pressed in Hyprland
|
|
#
|
|
# This script handles the transition from Desktop to Gaming Mode:
|
|
# 1. Masks suspend targets — prevents the system from sleeping when the
|
|
# monitor briefly disconnects during the display manager restart
|
|
# 2. Updates SDDM config to auto-login to the gaming session
|
|
# 3. Kills any leftover gamescope processes from a previous session
|
|
# 4. Switches to VT2 (virtual terminal) to avoid display conflicts
|
|
# 5. Restarts SDDM, which auto-logs into Gaming Mode
|
|
info "Creating switch-to-gaming script..."
|
|
local switch_script="/usr/local/bin/switch-to-gaming"
|
|
|
|
sudo tee "$switch_script" > /dev/null << 'SWITCH_SCRIPT'
|
|
#!/bin/bash
|
|
# Inhibit suspend FIRST - prevents suspend when monitor detaches during switch
|
|
sudo -n systemctl mask --runtime sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
|
|
sudo -n /usr/local/bin/gaming-session-switch gaming 2>/dev/null || {
|
|
notify-send -u critical -t 3000 "Gaming Mode" "Failed to update session config" 2>/dev/null || true
|
|
}
|
|
notify-send -u normal -t 2000 "Gaming Mode" "Switching to Gaming Mode..." 2>/dev/null || true
|
|
pkill -9 gamescope 2>/dev/null || true
|
|
pkill -9 -f gamescope-session 2>/dev/null || true
|
|
sleep 1
|
|
sudo -n chvt 2 2>/dev/null || true
|
|
sleep 0.3
|
|
sudo -n systemctl restart sddm
|
|
SWITCH_SCRIPT
|
|
|
|
sudo chmod +x "$switch_script"
|
|
info "Created $switch_script"
|
|
|
|
# switch-to-desktop — Called when Super+Shift+R is pressed in Gaming Mode
|
|
#
|
|
# This script handles the transition back from Gaming to Desktop Mode:
|
|
# 1. Unmasks suspend targets (re-enables sleep/hibernate)
|
|
# 2. Restores Bluetooth (disabled during gaming to reduce interference)
|
|
# 3. Gracefully shuts down Steam (timeout 5s, then force kill)
|
|
# 4. Kills gamescope with SIGTERM first, then SIGKILL if it won't die
|
|
# 5. Updates SDDM config back to Hyprland session
|
|
# 6. Restarts SDDM, which auto-logs into Desktop Mode
|
|
info "Creating switch-to-desktop script..."
|
|
local switch_desktop_script="/usr/local/bin/switch-to-desktop"
|
|
|
|
sudo tee "$switch_desktop_script" > /dev/null << 'SWITCH_DESKTOP'
|
|
#!/bin/bash
|
|
if [[ ! -f /tmp/.gaming-session-active ]]; then
|
|
exit 0
|
|
fi
|
|
rm -f /tmp/.gaming-session-active
|
|
|
|
sudo -n systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
|
|
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true
|
|
|
|
# Re-enable Bluetooth
|
|
sudo -n /usr/bin/rfkill unblock bluetooth 2>/dev/null || true
|
|
sudo -n /usr/bin/systemctl start bluetooth.service 2>/dev/null || true
|
|
|
|
timeout 5 steam -shutdown 2>/dev/null || true
|
|
sleep 1
|
|
|
|
pkill -TERM gamescope 2>/dev/null || true
|
|
pkill -TERM -f gamescope-session 2>/dev/null || true
|
|
|
|
for _ in {1..6}; do
|
|
pgrep -x gamescope >/dev/null 2>&1 || break
|
|
sleep 0.5
|
|
done
|
|
|
|
if pgrep -x gamescope >/dev/null 2>&1; then
|
|
pkill -9 gamescope 2>/dev/null || true
|
|
pkill -9 -f gamescope-session 2>/dev/null || true
|
|
fi
|
|
|
|
sleep 2
|
|
|
|
sudo -n chvt 2 2>/dev/null || true
|
|
sleep 0.5
|
|
sudo -n systemctl stop sddm 2>/dev/null || true
|
|
sleep 1
|
|
sudo -n systemctl start sddm &
|
|
disown
|
|
exit 0
|
|
SWITCH_DESKTOP
|
|
|
|
sudo chmod +x "$switch_desktop_script"
|
|
info "Created $switch_desktop_script"
|
|
|
|
# Keybind Monitor — Python daemon for Super+Shift+R in Gaming Mode
|
|
#
|
|
# Inside Gamescope, Hyprland isn't running so its keybinds don't work.
|
|
# This Python script uses python-evdev to read raw keyboard input directly
|
|
# from /dev/input/event* devices, bypassing the compositor entirely.
|
|
#
|
|
# It uses Linux's selector (epoll) interface to efficiently monitor multiple
|
|
# keyboard devices simultaneously without busy-waiting. When it detects
|
|
# Super+Shift+R, it calls switch-to-desktop to return to Hyprland.
|
|
#
|
|
# The user must be in the "input" group to read /dev/input/ devices.
|
|
info "Creating gaming mode keybind monitor..."
|
|
local keybind_monitor="/usr/local/bin/gaming-keybind-monitor"
|
|
|
|
sudo tee "$keybind_monitor" > /dev/null << 'KEYBIND_MONITOR'
|
|
#!/usr/bin/env python3
|
|
import sys
|
|
import subprocess
|
|
import time
|
|
import syslog
|
|
|
|
def log(msg, error=False):
|
|
print(msg, file=sys.stderr if error else sys.stdout)
|
|
syslog.syslog(syslog.LOG_ERR if error else syslog.LOG_INFO, msg)
|
|
|
|
syslog.openlog("gaming-keybind-monitor", syslog.LOG_PID)
|
|
|
|
try:
|
|
import evdev
|
|
from evdev import ecodes
|
|
except ImportError:
|
|
log("FATAL: python-evdev not installed", error=True)
|
|
sys.exit(1)
|
|
|
|
def find_keyboards():
|
|
keyboards = []
|
|
devices_checked = 0
|
|
permission_errors = 0
|
|
for path in evdev.list_devices():
|
|
devices_checked += 1
|
|
try:
|
|
device = evdev.InputDevice(path)
|
|
caps = device.capabilities()
|
|
if ecodes.EV_KEY in caps:
|
|
keys = caps[ecodes.EV_KEY]
|
|
if ecodes.KEY_A in keys and ecodes.KEY_R in keys:
|
|
keyboards.append(device)
|
|
except PermissionError:
|
|
permission_errors += 1
|
|
except Exception:
|
|
continue
|
|
if permission_errors > 0 and not keyboards:
|
|
log(f"FATAL: Permission denied on {permission_errors}/{devices_checked} input devices.", error=True)
|
|
return keyboards
|
|
|
|
def monitor_keyboards(keyboards):
|
|
meta_pressed = False
|
|
shift_pressed = False
|
|
from selectors import DefaultSelector, EVENT_READ
|
|
selector = DefaultSelector()
|
|
for kbd in keyboards:
|
|
selector.register(kbd, EVENT_READ)
|
|
log(f"Monitoring {len(keyboards)} keyboard(s) for Super+Shift+R...")
|
|
try:
|
|
while True:
|
|
for key, mask in selector.select():
|
|
device = key.fileobj
|
|
try:
|
|
for event in device.read():
|
|
if event.type != ecodes.EV_KEY:
|
|
continue
|
|
if event.code in (ecodes.KEY_LEFTMETA, ecodes.KEY_RIGHTMETA):
|
|
meta_pressed = event.value > 0
|
|
elif event.code in (ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT):
|
|
shift_pressed = event.value > 0
|
|
elif event.code == ecodes.KEY_R and event.value == 1:
|
|
if meta_pressed and shift_pressed:
|
|
log("Super+Shift+R detected! Switching to desktop...")
|
|
subprocess.run(['/usr/local/bin/switch-to-desktop'])
|
|
return
|
|
except Exception as e:
|
|
log(f"Read error: {e}", error=True)
|
|
continue
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
selector.close()
|
|
|
|
def main():
|
|
time.sleep(2)
|
|
keyboards = find_keyboards()
|
|
if not keyboards:
|
|
log("FATAL: No accessible keyboards found!", error=True)
|
|
sys.exit(1)
|
|
monitor_keyboards(keyboards)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
KEYBIND_MONITOR
|
|
|
|
sudo chmod +x "$keybind_monitor"
|
|
info "Created $keybind_monitor"
|
|
|
|
# SDDM Session Switching Config
|
|
#
|
|
# SDDM supports auto-login — it can automatically log in a user to a
|
|
# specific session without showing the login screen. This config file
|
|
# controls WHICH session SDDM auto-logs into.
|
|
#
|
|
# The switching mechanism works by editing this file:
|
|
# - "Session=hyprland-uwsm" → boots into Desktop Mode
|
|
# - "Session=gamescope-session-steam-nm" → boots into Gaming Mode
|
|
#
|
|
# The gaming-session-switch helper script toggles this value, then SDDM
|
|
# is restarted to pick up the change. The "zz-" prefix ensures this
|
|
# config loads LAST and overrides any other SDDM autologin settings.
|
|
info "Creating SDDM session switching config..."
|
|
local sddm_gaming_conf="/etc/sddm.conf.d/zz-gaming-session.conf"
|
|
|
|
local autologin_user="$current_user"
|
|
if [[ -f /etc/sddm.conf.d/autologin.conf ]]; then
|
|
autologin_user=$(sed -n 's/^User=//p' /etc/sddm.conf.d/autologin.conf 2>/dev/null | head -1)
|
|
[[ -z "$autologin_user" ]] && autologin_user="$current_user"
|
|
fi
|
|
|
|
sudo tee "$sddm_gaming_conf" > /dev/null << SDDM_GAMING
|
|
[Autologin]
|
|
User=${autologin_user}
|
|
Session=hyprland-uwsm
|
|
Relogin=true
|
|
SDDM_GAMING
|
|
|
|
info "Created $sddm_gaming_conf"
|
|
|
|
info "Creating session switching helper script..."
|
|
local session_helper="/usr/local/bin/gaming-session-switch"
|
|
|
|
sudo tee "$session_helper" > /dev/null << 'SESSION_HELPER'
|
|
#!/bin/bash
|
|
CONF="/etc/sddm.conf.d/zz-gaming-session.conf"
|
|
if [[ ! -f "$CONF" ]]; then
|
|
echo "Error: Config file not found: $CONF" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "$1" in
|
|
gaming)
|
|
sed -i 's/^Session=.*/Session=gamescope-session-steam-nm/' "$CONF"
|
|
echo "Session set to: gaming mode"
|
|
;;
|
|
desktop)
|
|
sed -i 's/^Session=.*/Session=hyprland-uwsm/' "$CONF"
|
|
echo "Session set to: desktop mode"
|
|
;;
|
|
*)
|
|
echo "Usage: $0 {gaming|desktop}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
SESSION_HELPER
|
|
|
|
sudo chmod +x "$session_helper"
|
|
info "Created $session_helper"
|
|
|
|
# Sudoers Rules for Session Switching
|
|
#
|
|
# The session switching scripts need to run several commands as root
|
|
# (restart SDDM, start/stop NetworkManager, control Bluetooth, etc.).
|
|
# These sudoers rules allow members of the "video" and "wheel" groups
|
|
# to run ONLY these specific commands without a password.
|
|
#
|
|
# This is much safer than giving blanket NOPASSWD sudo — each rule
|
|
# is scoped to a specific binary path. An attacker can't leverage
|
|
# these rules to run arbitrary commands as root.
|
|
local sudoers_session="/etc/sudoers.d/gaming-session-switch"
|
|
|
|
if [[ -f "$sudoers_session" ]]; then
|
|
info "Removing old sudoers rules to update..."
|
|
sudo rm -f "$sudoers_session"
|
|
fi
|
|
|
|
info "Creating sudoers rules for session switching..."
|
|
|
|
local switch_output
|
|
switch_output=$(sudo tee "$sudoers_session" << 'SUDOERS_SWITCH' 2>&1
|
|
%video ALL=(ALL) NOPASSWD: /usr/local/bin/gaming-session-switch
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart sddm
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/chvt
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl mask --runtime sleep.target suspend.target hibernate.target hybrid-sleep.target
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl start NetworkManager.service
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop NetworkManager.service
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl start bluetooth.service
|
|
%video ALL=(ALL) NOPASSWD: /usr/bin/rfkill unblock bluetooth
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-start
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-stop
|
|
SUDOERS_SWITCH
|
|
)
|
|
local switch_exit=$?
|
|
|
|
if [[ $switch_exit -eq 0 ]]; then
|
|
sudo chmod 0440 "$sudoers_session"
|
|
info "Sudoers rules created successfully"
|
|
else
|
|
err "Failed to create sudoers file (exit code: $switch_exit)"
|
|
fi
|
|
|
|
info "Adding Hyprland keybind..."
|
|
local hypr_bindings_conf="${user_home}/.config/hypr/bindings.conf"
|
|
|
|
if [[ ! -f "$hypr_bindings_conf" ]]; then
|
|
warn "bindings.conf not found at $hypr_bindings_conf - skipping keybind setup"
|
|
warn "You can manually add: bindd = SUPER SHIFT, S, Gaming Mode, exec, /usr/local/bin/switch-to-gaming"
|
|
else
|
|
if grep -q "switch-to-gaming" "$hypr_bindings_conf" 2>/dev/null; then
|
|
info "Gaming Mode keybind already exists in bindings.conf"
|
|
else
|
|
cat >> "$hypr_bindings_conf" << 'HYPR_GAMING'
|
|
|
|
bindd = SUPER SHIFT, S, Gaming Mode, exec, /usr/local/bin/switch-to-gaming
|
|
HYPR_GAMING
|
|
info "Added Gaming Mode keybind to bindings.conf"
|
|
fi
|
|
fi
|
|
|
|
info "Steam compatibility scripts provided by gamescope-session-steam-git"
|
|
|
|
info "Verifying NetworkManager integration..."
|
|
echo ""
|
|
|
|
local nm_test_ok=true
|
|
local iwd_was_active=false
|
|
systemctl is-active --quiet iwd.service && iwd_was_active=true
|
|
|
|
if ! systemctl is-active --quiet NetworkManager.service; then
|
|
info "Testing NetworkManager startup..."
|
|
if sudo systemctl start NetworkManager.service 2>/dev/null; then
|
|
sleep 2
|
|
if nmcli general status &>/dev/null; then
|
|
info "NetworkManager started successfully"
|
|
if nmcli general status 2>/dev/null | grep -qE "connected|connecting"; then
|
|
info "NetworkManager can see network - Steam network access should work"
|
|
else
|
|
warn "NetworkManager running but shows disconnected"
|
|
warn "This is expected if iwd/systemd-networkd manages your connection"
|
|
info "Steam should still be able to use the network via D-Bus"
|
|
fi
|
|
sudo systemctl stop NetworkManager.service 2>/dev/null || true
|
|
if $iwd_was_active; then
|
|
info "Restoring iwd WiFi connection..."
|
|
sudo systemctl restart iwd.service 2>/dev/null || true
|
|
sleep 2
|
|
fi
|
|
else
|
|
nm_test_ok=false
|
|
err "NetworkManager started but nmcli not responding"
|
|
sudo systemctl stop NetworkManager.service 2>/dev/null || true
|
|
if $iwd_was_active; then
|
|
sudo systemctl restart iwd.service 2>/dev/null || true
|
|
fi
|
|
fi
|
|
else
|
|
nm_test_ok=false
|
|
err "Failed to start NetworkManager for testing"
|
|
fi
|
|
else
|
|
info "NetworkManager already running - integration should work"
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SESSION SWITCHING CONFIGURED (ChimeraOS)"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Usage:"
|
|
echo " - Press Super+Shift+S in Hyprland to switch to Gaming Mode"
|
|
echo " - Press Super+Shift+R in Gaming Mode to return to Hyprland"
|
|
echo " - (Steam's Power > Exit to Desktop also works as fallback)"
|
|
echo ""
|
|
echo " ChimeraOS packages installed:"
|
|
echo " - gamescope-session-git (base session framework)"
|
|
echo " - gamescope-session-steam-git (Steam session)"
|
|
echo ""
|
|
echo " Files created/modified:"
|
|
echo " - ~/.config/environment.d/gamescope-session-plus.conf"
|
|
echo " - /usr/local/bin/gamescope-session-nm-wrapper"
|
|
echo " - /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"
|
|
echo " - /usr/lib/os-session-select"
|
|
echo " - /usr/local/bin/switch-to-gaming"
|
|
echo " - /usr/local/bin/switch-to-desktop"
|
|
echo " - /usr/local/bin/gaming-keybind-monitor (Super+Shift+R)"
|
|
echo " - ~/.config/hypr/bindings.conf (keybind added)"
|
|
echo ""
|
|
echo " NetworkManager integration (Steam network access):"
|
|
echo " - /usr/local/bin/gamescope-nm-start"
|
|
echo " - /usr/local/bin/gamescope-nm-stop"
|
|
echo " - /etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"
|
|
echo " - /etc/sudoers.d/gaming-session-switch (NM rules added)"
|
|
|
|
if [[ -f /etc/NetworkManager/conf.d/10-iwd-backend.conf ]]; then
|
|
echo " - /etc/NetworkManager/conf.d/10-iwd-backend.conf (iwd backend)"
|
|
fi
|
|
if [[ -f /etc/NetworkManager/conf.d/20-unmanaged-systemd.conf ]]; then
|
|
echo " - /etc/NetworkManager/conf.d/20-unmanaged-systemd.conf (systemd-networkd coexistence)"
|
|
fi
|
|
echo ""
|
|
|
|
if [[ "$nm_test_ok" != "true" ]]; then
|
|
echo " WARNING: NetworkManager test failed!"
|
|
echo " Steam may not have network access in Gaming Mode."
|
|
echo ""
|
|
echo " Troubleshooting:"
|
|
echo " 1. Ensure NetworkManager is installed: pacman -S networkmanager"
|
|
echo " 2. Check if iwd is running: systemctl status iwd"
|
|
echo " 3. Try manually: sudo systemctl start NetworkManager && nmcli general"
|
|
echo " 4. Check logs: journalctl -u NetworkManager -n 50"
|
|
echo ""
|
|
fi
|
|
|
|
if command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
|
|
hyprctl reload >/dev/null 2>&1 && info "Hyprland config reloaded" || true
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Comprehensive verification — checks every file, permission, package, group,
|
|
# and service that Gaming Mode depends on. This is the --verify mode that
|
|
# users can run to diagnose problems without re-running the full installer.
|
|
#
|
|
# It checks:
|
|
# - All installed files exist and have correct permissions
|
|
# - Hyprland keybind is configured
|
|
# - ChimeraOS AUR packages are installed
|
|
# - Steam library mount script and udisks2 are ready
|
|
# - python-evdev works and user is in the input group
|
|
# - Gamescope session config exists
|
|
# - User is in required groups (video, input, wheel)
|
|
# - Service states (NM should be inactive, iwd should be active)
|
|
# - Sudo permissions work without password
|
|
verify_installation() {
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " GAMING MODE INSTALLATION VERIFICATION"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
local all_ok=true
|
|
local missing_files=()
|
|
local permission_issues=()
|
|
|
|
declare -A expected_files=(
|
|
["/usr/local/bin/gamescope-session-nm-wrapper"]="755:ChimeraOS session with NM wrapper"
|
|
["/usr/local/lib/gamescope-nvidia/gamescope"]="755:NVIDIA gamescope wrapper (--force-composition)"
|
|
["/usr/local/bin/gaming-session-switch"]="755:Session switching helper (gaming/desktop)"
|
|
["/usr/lib/os-session-select"]="755:Steam Exit to Desktop handler"
|
|
["/usr/local/bin/switch-to-gaming"]="755:Hyprland to Gaming Mode switcher"
|
|
["/usr/local/bin/switch-to-desktop"]="755:Gaming Mode to Desktop switcher (Super+Shift+R)"
|
|
["/usr/local/bin/gaming-keybind-monitor"]="755:Keybind monitor for Super+Shift+R"
|
|
["/usr/local/bin/gamescope-nm-start"]="755:NetworkManager start script"
|
|
["/usr/local/bin/gamescope-nm-stop"]="755:NetworkManager stop script"
|
|
["/usr/local/bin/steam-library-mount"]="755:Steam library drive auto-mount script"
|
|
["/usr/bin/steamos-session-select"]="755:Steam compatibility (from AUR package)"
|
|
["/usr/bin/steamos-update"]="755:Steam compatibility (from AUR package)"
|
|
["/usr/bin/jupiter-biosupdate"]="755:Steam compatibility (from AUR package)"
|
|
["/usr/bin/steamos-select-branch"]="755:Steam compatibility (from AUR package)"
|
|
["/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"]="644:SDDM session entry"
|
|
["/usr/share/gamescope-session-plus/gamescope-session-plus"]="755:ChimeraOS session launcher (from AUR)"
|
|
["/etc/sddm.conf.d/zz-gaming-session.conf"]="644:SDDM session switching config"
|
|
["/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"]="644:Polkit NM rules"
|
|
["/etc/polkit-1/rules.d/50-udisks-gaming.rules"]="644:Polkit udisks2 rules (external drive mount)"
|
|
["/etc/sudoers.d/gaming-session-switch"]="440:Sudoers rules"
|
|
["/etc/NetworkManager/conf.d/10-iwd-backend.conf"]="644:NM iwd backend config (optional)"
|
|
["/etc/NetworkManager/conf.d/20-unmanaged-systemd.conf"]="644:NM systemd coexistence (optional)"
|
|
["/etc/udev/rules.d/99-gaming-performance.rules"]="644:Udev performance rules"
|
|
["/etc/sudoers.d/gaming-mode-sysctl"]="440:Performance sudoers"
|
|
["/etc/security/limits.d/99-gaming-memlock.conf"]="644:Memlock limits"
|
|
["/etc/pipewire/pipewire.conf.d/10-gaming-latency.conf"]="644:PipeWire low-latency"
|
|
["/etc/environment.d/99-shader-cache.conf"]="644:Shader cache config"
|
|
)
|
|
echo " FILE STATUS:"
|
|
echo " ------------"
|
|
echo ""
|
|
|
|
for file in "${!expected_files[@]}"; do
|
|
local expected_perm="${expected_files[$file]%%:*}"
|
|
local description="${expected_files[$file]#*:}"
|
|
local is_optional=false
|
|
|
|
[[ "$description" == *"(optional)"* ]] && is_optional=true
|
|
|
|
if sudo test -f "$file" 2>/dev/null; then
|
|
local actual_perm
|
|
actual_perm=$(sudo stat -c "%a" "$file" 2>/dev/null)
|
|
|
|
if [[ "$actual_perm" == "$expected_perm" ]]; then
|
|
printf " ✓ %-55s [%s] OK\n" "$file" "$actual_perm"
|
|
else
|
|
printf " ⚠ %-55s [%s] (expected %s)\n" "$file" "$actual_perm" "$expected_perm"
|
|
permission_issues+=("$file: has $actual_perm, expected $expected_perm")
|
|
all_ok=false
|
|
fi
|
|
else
|
|
if $is_optional; then
|
|
printf " - %-55s [SKIPPED] %s\n" "$file" "(optional)"
|
|
else
|
|
printf " ✗ %-55s [MISSING]\n" "$file"
|
|
missing_files+=("$file: $description")
|
|
all_ok=false
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo " HYPRLAND KEYBIND:"
|
|
echo " -----------------"
|
|
local hypr_bindings="$HOME/.config/hypr/bindings.conf"
|
|
if [[ -f "$hypr_bindings" ]]; then
|
|
if grep -q "switch-to-gaming" "$hypr_bindings" 2>/dev/null; then
|
|
echo " ✓ Gaming Mode keybind (Super+Shift+S) configured"
|
|
else
|
|
echo " ✗ Gaming Mode keybind NOT found in bindings.conf"
|
|
all_ok=false
|
|
fi
|
|
else
|
|
echo " ⚠ bindings.conf not found - keybind needs manual setup"
|
|
fi
|
|
|
|
echo ""
|
|
echo " CHIMERAOS PACKAGES:"
|
|
echo " -------------------"
|
|
if check_package "gamescope-session-git" || check_package "gamescope-session"; then
|
|
echo " ✓ gamescope-session installed"
|
|
else
|
|
echo " ✗ gamescope-session NOT installed"
|
|
all_ok=false
|
|
fi
|
|
if check_package "gamescope-session-steam-git" || check_package "gamescope-session-steam"; then
|
|
echo " ✓ gamescope-session-steam installed"
|
|
else
|
|
echo " ✗ gamescope-session-steam NOT installed"
|
|
all_ok=false
|
|
fi
|
|
|
|
echo ""
|
|
echo " STEAM LIBRARY DRIVE SUPPORT:"
|
|
echo " -----------------------------"
|
|
if [[ -x "/usr/local/bin/steam-library-mount" ]]; then
|
|
echo " ✓ steam-library-mount script installed"
|
|
else
|
|
echo " ✗ steam-library-mount NOT found - external Steam libraries will not auto-mount"
|
|
all_ok=false
|
|
fi
|
|
if check_package "udisks2"; then
|
|
echo " ✓ udisks2 installed (mount backend)"
|
|
else
|
|
echo " ✗ udisks2 NOT installed"
|
|
all_ok=false
|
|
fi
|
|
if sudo test -f "/etc/polkit-1/rules.d/50-udisks-gaming.rules" 2>/dev/null; then
|
|
echo " ✓ udisks2 polkit rules configured"
|
|
else
|
|
echo " ✗ udisks2 polkit rules NOT found"
|
|
all_ok=false
|
|
fi
|
|
|
|
echo ""
|
|
echo " KEYBIND MONITOR (Super+Shift+R):"
|
|
echo " ---------------------------------"
|
|
local keybind_ok=true
|
|
|
|
if check_package "python-evdev"; then
|
|
echo " ✓ python-evdev installed"
|
|
else
|
|
echo " ✗ python-evdev NOT installed"
|
|
keybind_ok=false
|
|
all_ok=false
|
|
fi
|
|
|
|
if python3 -c "import evdev" 2>/dev/null; then
|
|
echo " ✓ python-evdev importable"
|
|
else
|
|
echo " ✗ python-evdev cannot be imported"
|
|
keybind_ok=false
|
|
all_ok=false
|
|
fi
|
|
|
|
if groups 2>/dev/null | grep -qw input; then
|
|
echo " ✓ User in 'input' group"
|
|
else
|
|
echo " ✗ User NOT in 'input' group (required for keybind)"
|
|
keybind_ok=false
|
|
all_ok=false
|
|
fi
|
|
|
|
if ls /dev/input/event* >/dev/null 2>&1; then
|
|
local test_device=$(ls /dev/input/event* 2>/dev/null | head -1)
|
|
if [[ -r "$test_device" ]]; then
|
|
echo " ✓ Can read input devices"
|
|
else
|
|
echo " ✗ Cannot read $test_device (permission denied)"
|
|
echo " (May need to log out/in after adding to input group)"
|
|
keybind_ok=false
|
|
all_ok=false
|
|
fi
|
|
else
|
|
echo " ⚠ No /dev/input/event* devices found"
|
|
fi
|
|
|
|
if $keybind_ok; then
|
|
echo " → Super+Shift+R keybind should work"
|
|
else
|
|
echo " → Super+Shift+R keybind will NOT work (use Steam > Power > Exit to Desktop)"
|
|
fi
|
|
|
|
echo ""
|
|
echo " USER CONFIG:"
|
|
echo " ------------"
|
|
local user_conf="$HOME/.config/environment.d/gamescope-session-plus.conf"
|
|
if [[ -f "$user_conf" ]]; then
|
|
echo " ✓ gamescope-session-plus.conf exists"
|
|
else
|
|
echo " ✗ gamescope-session-plus.conf NOT found"
|
|
all_ok=false
|
|
fi
|
|
|
|
echo ""
|
|
echo " USER GROUPS:"
|
|
echo " ------------"
|
|
local user_groups
|
|
user_groups=$(groups 2>/dev/null)
|
|
for grp in video input wheel; do
|
|
if echo "$user_groups" | grep -qw "$grp"; then
|
|
printf " ✓ User is in '%s' group\n" "$grp"
|
|
else
|
|
printf " ✗ User is NOT in '%s' group\n" "$grp"
|
|
all_ok=false
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo " SERVICE STATUS:"
|
|
echo " ---------------"
|
|
echo " NetworkManager: $(systemctl is-active NetworkManager.service 2>/dev/null || echo 'inactive') (should be inactive until gaming mode)"
|
|
echo " iwd: $(systemctl is-active iwd.service 2>/dev/null || echo 'inactive')"
|
|
echo " systemd-networkd: $(systemctl is-active systemd-networkd.service 2>/dev/null || echo 'inactive')"
|
|
echo " polkit: $(systemctl is-active polkit.service 2>/dev/null || echo 'inactive')"
|
|
|
|
echo ""
|
|
echo " SUDO PERMISSIONS TEST:"
|
|
echo " ----------------------"
|
|
if sudo -n true 2>/dev/null; then
|
|
echo " ✓ sudo -n works (passwordless sudo available)"
|
|
if sudo -n -l /usr/local/bin/gamescope-nm-start &>/dev/null; then
|
|
echo " ✓ Can run gamescope-nm-start without password"
|
|
else
|
|
echo " ✗ Cannot run gamescope-nm-start without password"
|
|
all_ok=false
|
|
fi
|
|
else
|
|
echo " ⚠ sudo -n test skipped (requires recent sudo auth)"
|
|
echo " Run: sudo -v && sudo -n -l /usr/local/bin/gamescope-nm-start"
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
if $all_ok; then
|
|
echo " ✓ ALL CHECKS PASSED - Gaming Mode should work correctly"
|
|
else
|
|
echo " ⚠ SOME ISSUES DETECTED"
|
|
echo ""
|
|
if ((${#missing_files[@]})); then
|
|
echo " Missing files (${#missing_files[@]}):"
|
|
for f in "${missing_files[@]}"; do
|
|
echo " - $f"
|
|
done
|
|
fi
|
|
if ((${#permission_issues[@]})); then
|
|
echo ""
|
|
echo " Permission issues (${#permission_issues[@]}):"
|
|
for p in "${permission_issues[@]}"; do
|
|
echo " - $p"
|
|
done
|
|
fi
|
|
echo ""
|
|
echo " Re-run the installer to fix these issues."
|
|
fi
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
$all_ok && return 0 || return 1
|
|
}
|
|
|
|
# Main orchestrator — runs the full installation process in order.
|
|
# Each step builds on the previous one:
|
|
# 1. Authenticate sudo (and clear cached credentials first for a fresh prompt)
|
|
# 2. Validate we're on an Omarchy system
|
|
# 3. Install Steam dependencies and GPU drivers
|
|
# 4. Configure NVIDIA kernel parameters (if applicable)
|
|
# 5. Set NVIDIA environment variables (if applicable)
|
|
# 6. Install script requirements and performance permissions
|
|
# 7. Set up session switching (the big one — all the scripts and configs)
|
|
# 8. Prompt for reboot/relogin if needed
|
|
# 9. Optionally run verification to confirm everything worked
|
|
execute_setup() {
|
|
sudo -k
|
|
sudo -v || die "sudo authentication required"
|
|
|
|
validate_environment
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SUPER SHIFT S GAMING MODE INSTALLER v${Super_Shift_S_VERSION}"
|
|
echo " Dependencies & GPU Configuration"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
check_steam_dependencies
|
|
check_nvidia_kernel_params
|
|
install_nvidia_deckmode_env
|
|
setup_requirements
|
|
setup_session_switching
|
|
|
|
if [ "$NEEDS_REBOOT" -eq 1 ]; then
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " IMPORTANT: REBOOT REQUIRED"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Bootloader configuration has been updated (nvidia-drm.modeset=1)."
|
|
echo " You MUST reboot for the kernel parameter to take effect."
|
|
echo ""
|
|
if [ "$NEEDS_RELOGIN" -eq 1 ]; then
|
|
echo " Additionally, user groups were updated (video/input/wheel)."
|
|
fi
|
|
echo ""
|
|
read -p "Reboot now? [y/N]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
info "Rebooting..."
|
|
sleep 2
|
|
systemctl reboot
|
|
else
|
|
echo ""
|
|
echo " Remember to reboot before continuing!"
|
|
echo ""
|
|
fi
|
|
elif [ "$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 " for the changes to take effect."
|
|
echo ""
|
|
read -r -p "Press Enter to exit (remember to log out)..."
|
|
else
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SETUP COMPLETE"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Dependencies, GPU configuration, and session switching are ready."
|
|
echo ""
|
|
echo " To switch to Gaming Mode: Press Super+Shift+S"
|
|
echo " To return to Desktop: Press Super+Shift+R"
|
|
echo ""
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Run installation verification? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
verify_installation
|
|
fi
|
|
}
|
|
|
|
show_help() {
|
|
echo "Super Shift S Gaming Mode Installer v${Super_Shift_S_VERSION}"
|
|
echo ""
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --help, -h Show this help message"
|
|
echo " --verify, -v Run verification only (check all files and permissions)"
|
|
echo " --version Show version number"
|
|
echo ""
|
|
echo "Without options, runs the full installation/setup process."
|
|
echo ""
|
|
}
|
|
|
|
# Command-line argument parsing — determines what mode to run in.
|
|
# With no arguments, runs the full installation. Otherwise:
|
|
# --help/-h: Show usage information
|
|
# --verify/-v: Only check if everything is installed correctly
|
|
# --version: Print version number
|
|
case "${1:-}" in
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
--verify|-v)
|
|
echo "Running verification only..."
|
|
verify_installation
|
|
exit $?
|
|
;;
|
|
--version)
|
|
echo "Super Shift S Gaming Mode Installer v${Super_Shift_S_VERSION}"
|
|
exit 0
|
|
;;
|
|
"")
|
|
execute_setup
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
echo "Use --help for usage information."
|
|
exit 1
|
|
;;
|
|
esac
|