- GPU detection rewritten: xe-driver-only, bus-based iGPU/dGPU classification, generation tagging (alchemist/battlemage/xe2/xe3/other) for gen-specific workarounds - Selection ranks dGPU+display > dGPU > iGPU+display > iGPU - Removed: MESA MANAGEMENT and MESA-GIT REBUILD sections, USE_MESA_GIT config option, cleanup_on_exit trap (was only relevant during mesa swap) - Added: STEAM FIRST-RUN BOOTSTRAP — interactive Steam login before Gaming Mode - Added: is_omarchy() helper - Gen-specific wrapper env: INTEL_DEBUG=norbc + ANV_QUEUE_THREAD_DISABLE=1 only on Alchemist - README: refreshed support matrix, removed mesa-git/recovery sections, documented gen-aware workarounds
1639 lines
62 KiB
Bash
Executable file
1639 lines
62 KiB
Bash
Executable file
#!/bin/bash
|
|
#
|
|
# ARCGames - Gaming Mode Installer for Intel Arc GPUs
|
|
# Version: 1.6.0
|
|
#
|
|
# Description:
|
|
# Sets up a SteamOS-like gaming experience on Arch Linux with Hyprland.
|
|
# Supports Intel Arc discrete GPUs (Alchemist DG2, Battlemage) and modern
|
|
# Intel Arc-branded iGPUs (Lunar Lake Xe2, Panther Lake Xe3).
|
|
#
|
|
# Features:
|
|
# - Steam and gaming dependencies installation
|
|
# - Mesa-git or stable Mesa driver selection
|
|
# - Gamescope session switching (Hyprland <-> Gaming Mode)
|
|
# - Performance tuning (GPU, audio, memory)
|
|
# - External Steam library auto-mounting
|
|
#
|
|
# Usage:
|
|
# ./ARCGames_install.sh [--help|--version]
|
|
#
|
|
# Keybinds (after installation):
|
|
# Super+Shift+S - Switch to Gaming Mode (from Hyprland)
|
|
# Super+Shift+R - Return to Desktop (from Gaming Mode)
|
|
#
|
|
###############################################################################
|
|
|
|
set -uo pipefail
|
|
|
|
ARCGAMES_VERSION="1.6.0"
|
|
|
|
###############################################################################
|
|
# CONFIGURATION
|
|
###############################################################################
|
|
|
|
CONFIG_FILE="/etc/gaming-mode.conf"
|
|
# Note: REAL_HOME not yet defined here, check both locations
|
|
[[ -f "${HOME}/.gaming-mode.conf" ]] && CONFIG_FILE="${HOME}/.gaming-mode.conf"
|
|
[[ -n "${SUDO_USER:-}" ]] && {
|
|
_sudo_home=$(getent passwd "$SUDO_USER" | cut -d: -f6)
|
|
[[ -n "$_sudo_home" && -f "$_sudo_home/.gaming-mode.conf" ]] && CONFIG_FILE="$_sudo_home/.gaming-mode.conf"
|
|
}
|
|
|
|
# Parse config file safely (no arbitrary code execution)
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
while IFS='=' read -r _key _value; do
|
|
_key="${_key#"${_key%%[![:space:]]*}"}" # trim leading whitespace
|
|
_key="${_key%"${_key##*[![:space:]]}"}" # trim trailing whitespace
|
|
_value="${_value#"${_value%%[![:space:]]*}"}"
|
|
_value="${_value%"${_value##*[![:space:]]}"}"
|
|
case "$_key" in
|
|
PERFORMANCE_MODE) PERFORMANCE_MODE="$_value" ;;
|
|
esac
|
|
done < "$CONFIG_FILE" 2>/dev/null || true
|
|
fi
|
|
|
|
: "${PERFORMANCE_MODE:=enabled}"
|
|
|
|
# Global state
|
|
NEEDS_RELOGIN=0
|
|
INTEL_ARC_VK_DEVICE=""
|
|
INTEL_ARC_DRM_CARD=""
|
|
INTEL_GPU_TIER="" # dgpu | igpu
|
|
INTEL_GPU_GEN="" # alchemist | battlemage | xe2 | xe3 | other
|
|
|
|
# Resolve actual user (handles sudo case)
|
|
REAL_USER="${SUDO_USER:-$USER}"
|
|
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
|
|
|
###############################################################################
|
|
# UTILITY FUNCTIONS
|
|
###############################################################################
|
|
|
|
info() { echo "[*] $*"; }
|
|
warn() { echo "[!] $*" >&2; }
|
|
err() { echo "[!] $*" >&2; }
|
|
|
|
die() {
|
|
local msg="$1"
|
|
local code="${2:-1}"
|
|
echo "FATAL: $msg" >&2
|
|
logger -t arcgames "Installation failed: $msg"
|
|
exit "$code"
|
|
}
|
|
|
|
check_package() {
|
|
pacman -Qi "$1" &>/dev/null
|
|
}
|
|
|
|
check_aur_helper_functional() {
|
|
local helper="$1"
|
|
"$helper" --version &>/dev/null
|
|
}
|
|
|
|
is_omarchy() {
|
|
[[ -d "${REAL_HOME}/.local/share/omarchy" ]]
|
|
}
|
|
|
|
# Validate REAL_HOME was resolved (must be after die() is defined)
|
|
[[ -z "$REAL_HOME" ]] && die "Could not resolve home directory for user: $REAL_USER"
|
|
|
|
# Run command as the original user (handles case where script is run with sudo)
|
|
run_as_user() {
|
|
if [[ -n "${SUDO_USER:-}" ]] && [[ "$EUID" -eq 0 ]]; then
|
|
sudo -u "$SUDO_USER" "$@"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
###############################################################################
|
|
# ENVIRONMENT VALIDATION
|
|
###############################################################################
|
|
|
|
validate_environment() {
|
|
command -v pacman >/dev/null || die "pacman required"
|
|
command -v hyprctl >/dev/null || die "hyprctl required"
|
|
[[ -d "$REAL_HOME/.config/hypr" ]] || die "Hyprland config directory not found (~/.config/hypr)"
|
|
|
|
# Ensure lspci is available for GPU detection
|
|
if ! command -v lspci >/dev/null 2>&1; then
|
|
info "Installing pciutils for GPU detection..."
|
|
sudo pacman -S --needed --noconfirm pciutils || die "Failed to install pciutils"
|
|
fi
|
|
}
|
|
|
|
###############################################################################
|
|
# GPU DETECTION
|
|
###############################################################################
|
|
|
|
# Classify a DRM card as iGPU (on root complex) or dGPU.
|
|
# Bus location is authoritative — Intel may brand iGPUs as "Arc" (Lunar Lake,
|
|
# Panther Lake), so name patterns alone misclassify them.
|
|
is_intel_igpu() {
|
|
local card_path="$1" pci_slot=""
|
|
[[ -L "$card_path/device" ]] && pci_slot=$(basename "$(readlink -f "$card_path/device")")
|
|
[[ -z "$pci_slot" ]] && return 1
|
|
[[ "$pci_slot" =~ ^0000:00: ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Get the Vulkan device ID (vendor:device) for a PCI slot
|
|
get_vk_device_id() {
|
|
local pci_slot="$1"
|
|
local vendor device
|
|
|
|
vendor=$(cat "/sys/bus/pci/devices/$pci_slot/vendor" 2>/dev/null | sed 's/0x//')
|
|
device=$(cat "/sys/bus/pci/devices/$pci_slot/device" 2>/dev/null | sed 's/0x//')
|
|
|
|
if [[ -n "$vendor" && -n "$device" ]]; then
|
|
echo "${vendor}:${device}"
|
|
fi
|
|
}
|
|
|
|
# Map an Intel Vulkan device ID (vendor:device) to a generation tag.
|
|
# Used to gate gen-specific workarounds (e.g. norbc on Alchemist only).
|
|
detect_intel_gen() {
|
|
local vk_id="$1"
|
|
local dev="${vk_id#*:}"
|
|
case "$dev" in
|
|
4f8?|4f9?|56[89ab]?|56c?) echo "alchemist" ;; # DG2 (A-series)
|
|
e20?|e21?|e22?|e23?) echo "battlemage" ;; # BMG (B-series dGPU + iGPU)
|
|
64a?|64b?) echo "xe2" ;; # Lunar Lake iGPU
|
|
fd??|b0??) echo "xe3" ;; # Panther Lake iGPU (provisional)
|
|
*) echo "other" ;;
|
|
esac
|
|
}
|
|
|
|
# Walk Intel DRM cards and pick the best one for gaming.
|
|
# Preference: dGPU with connected display > dGPU > iGPU with display > iGPU.
|
|
find_intel_gpu() {
|
|
local best_card="" best_pci="" best_tier="" best_has_display=false
|
|
|
|
_consider() {
|
|
# $1=card $2=pci $3=tier $4=has_display
|
|
local rank_new rank_old
|
|
case "$3:$4" in
|
|
dgpu:true) rank_new=4 ;;
|
|
dgpu:false) rank_new=3 ;;
|
|
igpu:true) rank_new=2 ;;
|
|
igpu:false) rank_new=1 ;;
|
|
esac
|
|
case "$best_tier:$best_has_display" in
|
|
dgpu:true) rank_old=4 ;;
|
|
dgpu:false) rank_old=3 ;;
|
|
igpu:true) rank_old=2 ;;
|
|
igpu:false) rank_old=1 ;;
|
|
*) rank_old=0 ;;
|
|
esac
|
|
if (( rank_new > rank_old )); then
|
|
best_card="$1" best_pci="$2" best_tier="$3" best_has_display="$4"
|
|
fi
|
|
}
|
|
|
|
for card_path in /sys/class/drm/card[0-9]*; do
|
|
local card_name
|
|
card_name=$(basename "$card_path")
|
|
[[ "$card_name" == render* ]] && continue
|
|
|
|
local driver_link="$card_path/device/driver"
|
|
[[ -L "$driver_link" ]] || continue
|
|
local driver
|
|
driver=$(basename "$(readlink "$driver_link")")
|
|
# xe-only: skip i915-bound GPUs (older UHD/Iris). Arc dGPU + Arc-branded
|
|
# iGPU (Xe2 Lunar Lake, Xe3 Panther Lake) all use xe.
|
|
[[ "$driver" == "xe" ]] || continue
|
|
|
|
local pci_slot tier has_display=false
|
|
pci_slot=$(basename "$(readlink -f "$card_path/device")")
|
|
if is_intel_igpu "$card_path"; then tier=igpu; else tier=dgpu; fi
|
|
|
|
for connector in "$card_path"/"$card_name"-*/status; do
|
|
if [[ -f "$connector" ]] && grep -q "^connected$" "$connector" 2>/dev/null; then
|
|
has_display=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
info "Found Intel GPU: $card_name (tier=$tier, pci=$pci_slot, display=$has_display)"
|
|
_consider "$card_name" "$pci_slot" "$tier" "$has_display"
|
|
done
|
|
|
|
[[ -z "$best_card" ]] && return 1
|
|
|
|
INTEL_ARC_DRM_CARD="$best_card"
|
|
INTEL_ARC_VK_DEVICE=$(get_vk_device_id "$best_pci")
|
|
INTEL_GPU_TIER="$best_tier"
|
|
INTEL_GPU_GEN=$(detect_intel_gen "$INTEL_ARC_VK_DEVICE")
|
|
|
|
info "Selected: $INTEL_ARC_DRM_CARD (tier=$INTEL_GPU_TIER, gen=$INTEL_GPU_GEN, vk=$INTEL_ARC_VK_DEVICE)"
|
|
$best_has_display || warn "No monitor detected on selected GPU"
|
|
return 0
|
|
}
|
|
|
|
# Verify a usable Intel GPU is present and select one.
|
|
check_intel_gpu() {
|
|
local gpu_info
|
|
gpu_info=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' || echo "")
|
|
|
|
if ! echo "$gpu_info" | grep -iq intel; then
|
|
die "No Intel GPU detected. This script targets Intel Arc dGPUs and Arc-branded iGPUs."
|
|
fi
|
|
|
|
if ! find_intel_gpu; then
|
|
die "No xe-driven Intel GPU found.
|
|
This script targets Intel Arc (xe driver) only. Older UHD/Iris GPUs on i915
|
|
are intentionally ignored. Check 'lsmod | grep -E xe\\|i915' and 'lspci -k'."
|
|
fi
|
|
|
|
info "Intel GPU detected and selected: $INTEL_ARC_DRM_CARD ($INTEL_GPU_TIER/$INTEL_GPU_GEN)"
|
|
return 0
|
|
}
|
|
|
|
###############################################################################
|
|
# MULTILIB REPOSITORY
|
|
###############################################################################
|
|
|
|
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"
|
|
# Uncomment only the [multilib] header and its Include line
|
|
sudo sed -i '/^#\[multilib\]$/{s/^#//;n;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"
|
|
sudo pacman -Syu --noconfirm || die "Failed to update system"
|
|
else
|
|
die "Failed to enable multilib repository"
|
|
fi
|
|
}
|
|
|
|
|
|
###############################################################################
|
|
# STEAM DEPENDENCIES
|
|
###############################################################################
|
|
|
|
check_steam_dependencies() {
|
|
info "Checking Steam dependencies for Intel Arc..."
|
|
|
|
#---------------------------------------------------------------------------
|
|
# System Update
|
|
#---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SYSTEM UPDATE RECOMMENDED"
|
|
echo "================================================================"
|
|
echo ""
|
|
read -p "Upgrade system now? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
info "Upgrading system..."
|
|
sudo pacman -Syu --noconfirm || die "Failed to upgrade system"
|
|
fi
|
|
echo ""
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Multilib Repository Check
|
|
#---------------------------------------------------------------------------
|
|
local -a missing_deps=()
|
|
local -a optional_deps=()
|
|
local multilib_enabled=false
|
|
|
|
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)"
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " MULTILIB REPOSITORY REQUIRED"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Steam requires 32-bit libraries from the multilib repository."
|
|
echo ""
|
|
read -p "Enable multilib repository now? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
enable_multilib_repo
|
|
multilib_enabled=true
|
|
else
|
|
die "Multilib repository is required for Steam"
|
|
fi
|
|
fi
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Mesa Status (stable only)
|
|
# Install stable mesa if no mesa is present. If mesa-git is already
|
|
# installed (e.g. from a previous version of this script), leave it alone.
|
|
#---------------------------------------------------------------------------
|
|
local has_mesa=false has_lib32_mesa=false
|
|
if check_package "mesa-git"; then
|
|
has_mesa=true
|
|
info "Mesa: mesa-git already installed (leaving as-is)"
|
|
elif check_package "mesa"; then
|
|
has_mesa=true
|
|
info "Mesa: stable mesa already installed"
|
|
else
|
|
info "Mesa: not installed (will install stable)"
|
|
fi
|
|
if check_package "lib32-mesa-git" || check_package "lib32-mesa"; then
|
|
has_lib32_mesa=true
|
|
fi
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Define Package Lists
|
|
#---------------------------------------------------------------------------
|
|
local -a core_deps=(
|
|
"steam"
|
|
"lib32-vulkan-icd-loader"
|
|
"vulkan-icd-loader"
|
|
"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"
|
|
)
|
|
|
|
# Stable mesa packages: only added when no mesa is currently installed.
|
|
if ! $has_mesa; then
|
|
core_deps+=("mesa" "vulkan-intel" "vulkan-mesa-layers")
|
|
fi
|
|
if $multilib_enabled && ! $has_lib32_mesa; then
|
|
core_deps+=("lib32-mesa" "lib32-vulkan-intel" "lib32-vulkan-mesa-layers")
|
|
fi
|
|
|
|
local -a gpu_deps=(
|
|
"intel-media-driver"
|
|
"vulkan-tools"
|
|
)
|
|
|
|
local -a recommended_deps=(
|
|
"gamescope"
|
|
"mangohud"
|
|
"lib32-mangohud"
|
|
"proton-ge-custom-bin"
|
|
"proton-cachyos-slr"
|
|
"udisks2"
|
|
)
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Check Dependencies
|
|
#---------------------------------------------------------------------------
|
|
info "Checking core Steam dependencies..."
|
|
for dep in "${core_deps[@]}"; do
|
|
check_package "$dep" || missing_deps+=("$dep")
|
|
done
|
|
|
|
info "Checking Intel GPU dependencies..."
|
|
for dep in "${gpu_deps[@]}"; do
|
|
check_package "$dep" || missing_deps+=("$dep")
|
|
done
|
|
|
|
info "Checking recommended dependencies..."
|
|
for dep in "${recommended_deps[@]}"; do
|
|
check_package "$dep" || optional_deps+=("$dep")
|
|
done
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Display Results
|
|
#---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " STEAM DEPENDENCY CHECK RESULTS"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Install Missing Packages
|
|
#---------------------------------------------------------------------------
|
|
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 --noconfirm "${missing_deps[@]}" || die "Failed to install dependencies"
|
|
info "Required dependencies installed successfully"
|
|
else
|
|
die "Missing required Steam dependencies"
|
|
fi
|
|
else
|
|
info "All required pacman dependencies are installed!"
|
|
fi
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Install Optional Packages
|
|
#---------------------------------------------------------------------------
|
|
echo ""
|
|
if ((${#optional_deps[@]})); then
|
|
echo " RECOMMENDED PACKAGES (${#optional_deps[@]}):"
|
|
for dep in "${optional_deps[@]}"; do
|
|
echo " - $dep"
|
|
done
|
|
echo ""
|
|
|
|
read -p "Install recommended packages? [y/N]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
info "Installing recommended packages..."
|
|
|
|
local -a pacman_optional=()
|
|
local -a aur_optional=()
|
|
for dep in "${optional_deps[@]}"; do
|
|
if pacman -Si "$dep" &>/dev/null; then
|
|
pacman_optional+=("$dep")
|
|
else
|
|
aur_optional+=("$dep")
|
|
fi
|
|
done
|
|
|
|
if ((${#pacman_optional[@]})); then
|
|
sudo pacman -S --needed --noconfirm "${pacman_optional[@]}" || info "Some optional packages failed"
|
|
fi
|
|
|
|
if ((${#aur_optional[@]})); then
|
|
local aur_helper=""
|
|
command -v yay >/dev/null 2>&1 && check_aur_helper_functional yay && aur_helper="yay"
|
|
[[ -z "$aur_helper" ]] && command -v paru >/dev/null 2>&1 && check_aur_helper_functional paru && aur_helper="paru"
|
|
|
|
if [[ -n "$aur_helper" ]]; then
|
|
read -p "Install AUR packages with $aur_helper? [y/N]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
run_as_user "$aur_helper" -S --needed --noconfirm "${aur_optional[@]}" || info "Some AUR packages failed"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
check_steam_config
|
|
}
|
|
|
|
###############################################################################
|
|
# STEAM CONFIGURATION
|
|
###############################################################################
|
|
|
|
check_steam_config() {
|
|
info "Checking Steam configuration..."
|
|
|
|
# Check the real user's groups (not root's when run with sudo)
|
|
local user_groups
|
|
user_groups=$(id -Gn "$REAL_USER" 2>/dev/null || groups "$REAL_USER" 2>/dev/null || echo "")
|
|
|
|
local missing_groups=()
|
|
echo "$user_groups" | grep -qw 'video' || missing_groups+=("video")
|
|
echo "$user_groups" | grep -qw 'input' || missing_groups+=("input")
|
|
echo "$user_groups" | grep -qw 'wheel' || missing_groups+=("wheel")
|
|
|
|
if ((${#missing_groups[@]})); then
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " USER GROUP PERMISSIONS"
|
|
echo "================================================================"
|
|
echo ""
|
|
read -p "Add $REAL_USER to ${missing_groups[*]} group(s)? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
local groups_to_add
|
|
groups_to_add=$(IFS=,; echo "${missing_groups[*]}")
|
|
if sudo usermod -aG "$groups_to_add" "$REAL_USER"; then
|
|
info "Successfully added $REAL_USER to group(s): $groups_to_add"
|
|
NEEDS_RELOGIN=1
|
|
fi
|
|
fi
|
|
else
|
|
info "User $REAL_USER is in video, input, and wheel groups - permissions OK"
|
|
fi
|
|
}
|
|
|
|
###############################################################################
|
|
# STEAM FIRST-RUN BOOTSTRAP
|
|
###############################################################################
|
|
|
|
# Detect whether Steam has been initialized for $REAL_USER (logged in once).
|
|
# Checks both the canonical and symlinked loginusers.vdf locations.
|
|
steam_already_bootstrapped() {
|
|
local f
|
|
for f in "${REAL_HOME}/.local/share/Steam/config/loginusers.vdf" \
|
|
"${REAL_HOME}/.steam/steam/config/loginusers.vdf"; do
|
|
[[ -s "$f" ]] && grep -q '"AccountName"' "$f" 2>/dev/null && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Run Steam in the user's graphical session, blocking until the window closes.
|
|
# Handles three invocation modes: as the user (just exec), under sudo with the
|
|
# user's $DISPLAY/$WAYLAND_DISPLAY visible (preserve env), or under sudo with
|
|
# only $SUDO_USER (reconstruct XDG_RUNTIME_DIR + bus path from UID).
|
|
launch_steam_for_user() {
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
steam
|
|
return $?
|
|
fi
|
|
|
|
[[ -z "${SUDO_USER:-}" ]] && { warn "Cannot launch Steam as root with no SUDO_USER"; return 1; }
|
|
local uid; uid=$(id -u "$REAL_USER")
|
|
sudo -u "$REAL_USER" --preserve-env=DISPLAY,WAYLAND_DISPLAY,XDG_SESSION_TYPE \
|
|
env XDG_RUNTIME_DIR="/run/user/${uid}" \
|
|
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" \
|
|
steam
|
|
}
|
|
|
|
bootstrap_steam_login() {
|
|
if steam_already_bootstrapped; then
|
|
info "Steam already initialized for $REAL_USER (loginusers.vdf has account entry)"
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v steam >/dev/null 2>&1; then
|
|
warn "steam command not found — skipping bootstrap. Re-run the installer once Steam is installed."
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " STEAM FIRST-RUN BOOTSTRAP"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " Gaming Mode (gamescope-session-steam) expects Steam to be"
|
|
echo " initialized and logged in. We'll launch Steam now so you can:"
|
|
echo ""
|
|
echo " 1. Wait for it to download/install runtime files"
|
|
echo " 2. Log in with your Steam account"
|
|
echo " 3. Close the Steam window when finished"
|
|
echo ""
|
|
echo " This script resumes automatically after Steam exits."
|
|
echo ""
|
|
read -p "Launch Steam now? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
warn "Skipped Steam bootstrap. Gaming Mode may fail to start until"
|
|
warn "you launch Steam at least once and log in."
|
|
return 0
|
|
fi
|
|
|
|
info "Launching Steam — close the window when finished..."
|
|
launch_steam_for_user || warn "Steam exited with non-zero status (continuing anyway)"
|
|
|
|
if steam_already_bootstrapped; then
|
|
info "Steam bootstrap complete — login verified"
|
|
else
|
|
warn "Steam closed but no login was detected. Gaming Mode may not"
|
|
warn "work until you've logged in at least once. Re-run the installer"
|
|
warn "or launch Steam manually to retry."
|
|
fi
|
|
}
|
|
|
|
###############################################################################
|
|
# PERFORMANCE CONFIGURATION
|
|
###############################################################################
|
|
|
|
setup_performance_permissions() {
|
|
local udev_rules_file="/etc/udev/rules.d/99-gaming-performance.rules"
|
|
local sudoers_file="/etc/sudoers.d/gaming-mode-sysctl"
|
|
|
|
if [[ -f "$udev_rules_file" ]] && [[ -f "$sudoers_file" ]]; then
|
|
info "Performance permissions already configured"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " PERFORMANCE PERMISSIONS SETUP"
|
|
echo "================================================================"
|
|
echo ""
|
|
read -p "Set up passwordless performance controls? [Y/n]: " -n 1 -r
|
|
echo
|
|
|
|
[[ $REPLY =~ ^[Nn]$ ]] && { info "Skipping permissions setup"; return 0; }
|
|
|
|
# Udev rules for GPU frequency control (xe driver only).
|
|
# Both flat (early xe: gt_*_freq_mhz) and per-tile (newer xe on Xe2/Xe3:
|
|
# device/tile0/gt0/freq0/*) sysfs layouts exist depending on kernel + hardware.
|
|
# The chmod helper tolerates absent files so a single rule covers both.
|
|
if [[ ! -f "$udev_rules_file" ]]; then
|
|
info "Creating udev rules for Intel xe GPU performance control..."
|
|
sudo tee "$udev_rules_file" > /dev/null <<'UDEV_RULES'
|
|
# Gaming Mode Performance Control Rules - Intel xe driver
|
|
# Group-writable (video group) instead of world-writable for security.
|
|
KERNEL=="cpu[0-9]*", SUBSYSTEM=="cpu", ACTION=="add", RUN+="/bin/sh -c 'chgrp video /sys/devices/system/cpu/%k/cpufreq/scaling_governor 2>/dev/null && chmod 664 /sys/devices/system/cpu/%k/cpufreq/scaling_governor 2>/dev/null; :'"
|
|
# Flat layout (early xe): /sys/class/drm/card*/gt_{boost,min,max}_freq_mhz
|
|
KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="xe", ACTION=="add", RUN+="/bin/sh -c 'for f in gt_boost_freq_mhz gt_min_freq_mhz gt_max_freq_mhz; do [ -e /sys/class/drm/%k/$f ] && chgrp video /sys/class/drm/%k/$f && chmod 664 /sys/class/drm/%k/$f; done; :'"
|
|
# Per-tile layout (xe on Xe2/Xe3 multi-tile): device/tile*/gt*/freq*/{min,max}_freq
|
|
KERNEL=="card[0-9]", SUBSYSTEM=="drm", DRIVERS=="xe", ACTION=="add", RUN+="/bin/sh -c 'for f in /sys/class/drm/%k/device/tile*/gt*/freq*/min_freq /sys/class/drm/%k/device/tile*/gt*/freq*/max_freq; do [ -e $f ] && chgrp video $f && chmod 664 $f; done; :'"
|
|
UDEV_RULES
|
|
sudo udevadm control --reload-rules || true
|
|
sudo udevadm trigger --subsystem-match=cpu --subsystem-match=drm || true
|
|
fi
|
|
|
|
# Sudoers rules for sysctl
|
|
if [[ ! -f "$sudoers_file" ]]; then
|
|
info "Creating sudoers rule for Performance Mode..."
|
|
sudo tee "$sudoers_file" > /dev/null << 'SUDOERS_PERF'
|
|
# Gaming Mode - Allow passwordless sysctl for performance tuning
|
|
%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=*
|
|
SUDOERS_PERF
|
|
sudo chmod 0440 "$sudoers_file"
|
|
fi
|
|
|
|
# Memory lock limits
|
|
local memlock_file="/etc/security/limits.d/99-gaming-memlock.conf"
|
|
if [[ ! -f "$memlock_file" ]]; then
|
|
info "Creating memlock limits..."
|
|
# Set memlock to ~25% of total RAM (in KB)
|
|
local total_ram_kb
|
|
total_ram_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
|
|
local memlock_kb=$(( total_ram_kb / 4 ))
|
|
# Clamp: minimum 2GB, maximum 16GB
|
|
(( memlock_kb < 2097152 )) && memlock_kb=2097152
|
|
(( memlock_kb > 16777216 )) && memlock_kb=16777216
|
|
info "Setting memlock to $(( memlock_kb / 1024 ))MB (based on $(( total_ram_kb / 1024 ))MB total RAM)"
|
|
sudo tee "$memlock_file" > /dev/null << MEMLOCKCONF
|
|
# Gaming memlock limits (auto-calculated: ~25% of total RAM)
|
|
* soft memlock ${memlock_kb}
|
|
* hard memlock ${memlock_kb}
|
|
MEMLOCKCONF
|
|
fi
|
|
|
|
# PipeWire low-latency config
|
|
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 configuration..."
|
|
sudo mkdir -p "$pipewire_conf_dir"
|
|
sudo tee "$pipewire_conf" > /dev/null << 'PIPEWIRECONF'
|
|
# Low-latency PipeWire tuning
|
|
context.properties = {
|
|
default.clock.min-quantum = 256
|
|
}
|
|
PIPEWIRECONF
|
|
fi
|
|
|
|
info "Performance permissions configured"
|
|
}
|
|
|
|
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 ""
|
|
read -p "Configure shader cache optimization? [Y/n]: " -n 1 -r
|
|
echo
|
|
|
|
[[ $REPLY =~ ^[Nn]$ ]] && return 0
|
|
|
|
info "Creating shader cache configuration..."
|
|
sudo mkdir -p /etc/environment.d
|
|
sudo tee "$env_file" > /dev/null << 'SHADERCACHE'
|
|
# Shader cache tuning for Intel Arc
|
|
MESA_SHADER_CACHE_MAX_SIZE=12G
|
|
MESA_SHADER_CACHE_DISABLE_CLEANUP=1
|
|
__GL_SHADER_DISK_CACHE=1
|
|
__GL_SHADER_DISK_CACHE_SIZE=12884901888
|
|
__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1
|
|
DXVK_STATE_CACHE=1
|
|
SHADERCACHE
|
|
sudo chmod 644 "$env_file"
|
|
info "Shader cache configured for Intel Arc"
|
|
}
|
|
|
|
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 --noconfirm "${packages_to_install[@]}" || die "package install failed"
|
|
else
|
|
die "Required packages missing"
|
|
fi
|
|
else
|
|
info "All required packages present."
|
|
fi
|
|
|
|
setup_performance_permissions
|
|
setup_shader_cache
|
|
|
|
# Grant cap_sys_nice to gamescope
|
|
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 ""
|
|
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
|
|
###############################################################################
|
|
|
|
setup_session_switching() {
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SESSION SWITCHING SETUP (Hyprland <-> Gamescope)"
|
|
echo " Intel Arc / Xe Gaming Configuration"
|
|
echo "================================================================"
|
|
echo ""
|
|
read -p "Set up session switching? [Y/n]: " -n 1 -r
|
|
echo
|
|
[[ $REPLY =~ ^[Nn]$ ]] && return 0
|
|
|
|
# Use global REAL_USER and REAL_HOME for consistency
|
|
local current_user="$REAL_USER"
|
|
local user_home="$REAL_HOME"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Detect Monitor Resolution
|
|
#---------------------------------------------------------------------------
|
|
local monitor_width=1920
|
|
local monitor_height=1080
|
|
local monitor_refresh=60
|
|
local monitor_output=""
|
|
|
|
if command -v hyprctl >/dev/null 2>&1; then
|
|
local monitor_json
|
|
monitor_json=$(hyprctl monitors -j 2>/dev/null)
|
|
if [[ -n "$monitor_json" ]]; then
|
|
if command -v jq >/dev/null 2>&1; then
|
|
monitor_width=$(echo "$monitor_json" | jq -r '.[0].width // 1920') || monitor_width=1920
|
|
monitor_height=$(echo "$monitor_json" | jq -r '.[0].height // 1080') || monitor_height=1080
|
|
monitor_refresh=$(echo "$monitor_json" | jq -r '.[0].refreshRate // 60 | floor') || monitor_refresh=60
|
|
monitor_output=$(echo "$monitor_json" | jq -r '.[0].name // ""') || monitor_output=""
|
|
else
|
|
# Fallback: parse JSON without jq
|
|
monitor_width=$(echo "$monitor_json" | grep -o '"width":[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$') || monitor_width=1920
|
|
monitor_height=$(echo "$monitor_json" | grep -o '"height":[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$') || monitor_height=1080
|
|
# refreshRate can be decimal (e.g., 143.998), extract integer part
|
|
monitor_refresh=$(echo "$monitor_json" | grep -o '"refreshRate":[[:space:]]*[0-9.]*' | head -1 | grep -o '[0-9.]*$' | cut -d. -f1) || monitor_refresh=60
|
|
monitor_output=$(echo "$monitor_json" | grep -o '"name":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') || monitor_output=""
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
info "Detected display: ${monitor_width}x${monitor_height}@${monitor_refresh}Hz${monitor_output:+ on $monitor_output}"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Install ChimeraOS Packages
|
|
#---------------------------------------------------------------------------
|
|
info "Checking for ChimeraOS gamescope-session packages..."
|
|
|
|
local -a aur_packages=()
|
|
local -a packages_to_remove=()
|
|
local -a steam_compat_scripts=(
|
|
"/usr/bin/steamos-session-select"
|
|
"/usr/bin/steamos-update"
|
|
"/usr/bin/jupiter-biosupdate"
|
|
"/usr/bin/steamos-select-branch"
|
|
)
|
|
|
|
# Check gamescope-session base package
|
|
if ! check_package "gamescope-session-git" && ! check_package "gamescope-session"; then
|
|
aur_packages+=("gamescope-session-git")
|
|
fi
|
|
|
|
# Check gamescope-session-steam package
|
|
if ! check_package "gamescope-session-steam-git"; then
|
|
if check_package "gamescope-session-steam"; then
|
|
warn "gamescope-session-steam (non-git) is installed but may be missing Steam compatibility scripts"
|
|
local scripts_missing=false
|
|
for script in "${steam_compat_scripts[@]}"; do
|
|
[[ ! -f "$script" ]] && { scripts_missing=true; break; }
|
|
done
|
|
$scripts_missing && packages_to_remove+=("gamescope-session-steam")
|
|
fi
|
|
aur_packages+=("gamescope-session-steam-git")
|
|
else
|
|
local scripts_missing=false
|
|
for script in "${steam_compat_scripts[@]}"; do
|
|
if [[ ! -f "$script" ]]; then
|
|
warn "gamescope-session-steam-git is installed but $script is missing!"
|
|
scripts_missing=true
|
|
break
|
|
fi
|
|
done
|
|
if $scripts_missing; then
|
|
warn "Reinstalling gamescope-session-steam-git to fix missing scripts..."
|
|
packages_to_remove+=("gamescope-session-steam-git")
|
|
aur_packages+=("gamescope-session-steam-git")
|
|
fi
|
|
fi
|
|
|
|
# Find AUR helper
|
|
local aur_helper=""
|
|
command -v yay >/dev/null 2>&1 && check_aur_helper_functional yay && aur_helper="yay"
|
|
[[ -z "$aur_helper" ]] && command -v paru >/dev/null 2>&1 && check_aur_helper_functional paru && aur_helper="paru"
|
|
|
|
# Remove problematic packages
|
|
if ((${#packages_to_remove[@]})) && [[ -n "$aur_helper" ]]; then
|
|
info "Removing incomplete packages: ${packages_to_remove[*]}"
|
|
sudo pacman -Rns --noconfirm "${packages_to_remove[@]}" 2>/dev/null || true
|
|
fi
|
|
|
|
# Install missing packages
|
|
if ((${#aur_packages[@]})); then
|
|
if [[ -n "$aur_helper" ]]; then
|
|
read -p "Install ChimeraOS session packages with $aur_helper? [Y/n]: " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
|
info "Installing ChimeraOS gamescope-session packages..."
|
|
run_as_user "$aur_helper" -S --noconfirm --overwrite '/usr/share/gamescope-session*' --overwrite '/usr/bin/steamos-*' --answeredit None --answerclean None --answerdiff None "${aur_packages[@]}" || \
|
|
err "Failed to install gamescope-session packages"
|
|
fi
|
|
else
|
|
warn "No AUR helper found (yay/paru). Please install manually: ${aur_packages[*]}"
|
|
fi
|
|
else
|
|
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
|
|
fi
|
|
|
|
#---------------------------------------------------------------------------
|
|
# NetworkManager Integration
|
|
#---------------------------------------------------------------------------
|
|
info "Setting up NetworkManager integration..."
|
|
|
|
if systemctl is-active --quiet iwd; then
|
|
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
|
|
fi
|
|
|
|
# NM start script
|
|
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"
|
|
if ! systemctl is-active --quiet NetworkManager.service; then
|
|
if systemctl start NetworkManager.service; then
|
|
touch "$NM_MARKER"
|
|
for _ in {1..20}; do
|
|
nmcli general status &>/dev/null && break
|
|
sleep 0.5
|
|
done
|
|
fi
|
|
fi
|
|
NM_START
|
|
sudo chmod +x "$nm_start_script"
|
|
|
|
# NM stop script - also restores iwd and bluetooth after gaming session
|
|
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"
|
|
if [ -f "$NM_MARKER" ]; then
|
|
rm -f "$NM_MARKER"
|
|
systemctl stop NetworkManager.service 2>/dev/null || true
|
|
fi
|
|
|
|
# Restore iwd (WiFi) and bluetooth if they are enabled but got disrupted
|
|
if systemctl is-enabled --quiet iwd.service 2>/dev/null; then
|
|
systemctl restart iwd.service 2>/dev/null || true
|
|
fi
|
|
if systemctl is-enabled --quiet bluetooth.service 2>/dev/null; then
|
|
systemctl restart bluetooth.service 2>/dev/null || true
|
|
fi
|
|
NM_STOP
|
|
sudo chmod +x "$nm_stop_script"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Polkit Rules
|
|
#---------------------------------------------------------------------------
|
|
local polkit_created=false
|
|
|
|
local polkit_rules="/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"
|
|
if ! sudo test -f "$polkit_rules"; then
|
|
sudo mkdir -p /etc/polkit-1/rules.d
|
|
sudo tee "$polkit_rules" > /dev/null << 'POLKIT_RULES'
|
|
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
|
|
sudo chmod 644 "$polkit_rules"
|
|
polkit_created=true
|
|
fi
|
|
|
|
local udisks_polkit="/etc/polkit-1/rules.d/50-udisks-gaming.rules"
|
|
if ! sudo test -f "$udisks_polkit"; then
|
|
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
|
|
sudo chmod 644 "$udisks_polkit"
|
|
polkit_created=true
|
|
fi
|
|
|
|
$polkit_created && { sudo systemctl restart polkit.service 2>/dev/null || true; info "Polkit rules created"; }
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Gamescope Session Configuration
|
|
#---------------------------------------------------------------------------
|
|
info "Creating gamescope-session-plus configuration for Intel Arc..."
|
|
|
|
local env_dir="${user_home}/.config/environment.d"
|
|
local gamescope_conf="${env_dir}/gamescope-session-plus.conf"
|
|
run_as_user mkdir -p "$env_dir"
|
|
|
|
run_as_user tee "$gamescope_conf" > /dev/null << GAMESCOPE_CONF
|
|
# Gamescope Session Plus Configuration
|
|
# Generated by ARCGames Installer v${ARCGAMES_VERSION}
|
|
# NOTE: environment.d format does NOT use 'export' keyword.
|
|
# Variables here apply to the systemd --user session (so also to Hyprland).
|
|
# Gen-specific Intel workarounds live in the gamescope wrapper instead.
|
|
# OUTPUT_CONNECTOR is picked dynamically at session start by
|
|
# gaming-pick-connector (lid-state aware) so plugging an external display
|
|
# in/out doesn't require reinstalling.
|
|
|
|
# Adaptive sync / VRR disabled
|
|
ADAPTIVE_SYNC=0
|
|
|
|
# Generic mesa/Vulkan tuning (safe across all Intel gens)
|
|
DISABLE_LAYER_MESA_ANTI_LAG=1
|
|
VKD3D_CONFIG=dxr11,dxr
|
|
mesa_glthread=true
|
|
|
|
# Storage and drive management
|
|
STEAM_ALLOW_DRIVE_UNMOUNT=1
|
|
|
|
# Misc
|
|
FCITX_NO_WAYLAND_DIAGNOSE=1
|
|
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
|
|
GAMESCOPE_CONF
|
|
info "Created $gamescope_conf"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Session Wrapper Script
|
|
#---------------------------------------------------------------------------
|
|
# Build gen-specific Intel env block at install time so the wrapper is
|
|
# static at runtime. norbc is an Alchemist-only RBC artifact workaround;
|
|
# ANV_QUEUE_THREAD_DISABLE was a stability hack from old mesa and hurts
|
|
# perf on Battlemage/Xe2/Xe3 with current drivers.
|
|
local intel_env_lines=""
|
|
case "$INTEL_GPU_GEN" in
|
|
alchemist)
|
|
intel_env_lines='export INTEL_DEBUG=norbc'$'\n''export ANV_QUEUE_THREAD_DISABLE=1'
|
|
;;
|
|
battlemage|xe2|xe3)
|
|
intel_env_lines='# No gen-specific workarounds needed for '"$INTEL_GPU_GEN"
|
|
;;
|
|
*)
|
|
intel_env_lines='# Unknown Intel gen ('"${INTEL_GPU_GEN:-unset}"') - no workarounds applied'
|
|
;;
|
|
esac
|
|
|
|
local nm_wrapper="/usr/local/bin/gamescope-session-nm-wrapper"
|
|
sudo tee "$nm_wrapper" > /dev/null << NM_WRAPPER
|
|
#!/bin/bash
|
|
# Gamescope session wrapper (NM + keybind monitor)
|
|
# Intel gen detected at install time: ${INTEL_GPU_GEN:-unknown} (${INTEL_GPU_TIER:-unknown})
|
|
|
|
# Intel gen-specific environment (gamescope-only, does not leak to desktop)
|
|
${intel_env_lines}
|
|
|
|
log() { logger -t gamescope-wrapper "\$*"; echo "\$*"; }
|
|
|
|
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
|
|
rm -f /tmp/.gaming-session-active
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Start NetworkManager
|
|
sudo -n /usr/local/bin/gamescope-nm-start 2>/dev/null || {
|
|
log "Warning: Could not start NetworkManager - Steam network features may not work"
|
|
}
|
|
|
|
# Start Steam library drive auto-mounter
|
|
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 - external Steam libraries will not auto-mount"
|
|
fi
|
|
|
|
# Mark gaming session active
|
|
echo "gamescope" > /tmp/.gaming-session-active
|
|
|
|
# Pre-flight check for keybind monitor
|
|
keybind_ok=true
|
|
|
|
if ! python3 -c "import evdev" 2>/dev/null; then
|
|
log "WARNING: python-evdev not installed - Super+Shift+R keybind disabled"
|
|
log "Fix: sudo pacman -S python-evdev"
|
|
keybind_ok=false
|
|
fi
|
|
|
|
if ! groups | grep -qw input; then
|
|
log "WARNING: User not in 'input' group - Super+Shift+R keybind disabled"
|
|
log "Fix: sudo usermod -aG input \$USER && log out/in"
|
|
keybind_ok=false
|
|
fi
|
|
|
|
if \$keybind_ok && ! ls /dev/input/event* >/dev/null 2>&1; then
|
|
log "WARNING: No input devices accessible - Super+Shift+R keybind disabled"
|
|
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 - use Steam > Power > Exit to Desktop instead"
|
|
fi
|
|
|
|
# Steam-specific environment variables
|
|
export QT_IM_MODULE=steam
|
|
export GTK_IM_MODULE=Steam
|
|
export STEAM_DISABLE_AUDIO_DEVICE_SWITCHING=1
|
|
export STEAM_ENABLE_VOLUME_HANDLER=1
|
|
|
|
# Pick output connector at runtime (lid state + display connection).
|
|
# Honors a manual override via /etc/gaming-mode.conf or ~/.gaming-mode.conf
|
|
# if FORCE_OUTPUT_CONNECTOR=<name> is set.
|
|
gm_force=""
|
|
for gm_conf in /etc/gaming-mode.conf "\$HOME/.gaming-mode.conf"; do
|
|
[[ -r "\$gm_conf" ]] || continue
|
|
val=\$(awk -F= '/^[[:space:]]*FORCE_OUTPUT_CONNECTOR[[:space:]]*=/ {gsub(/[[:space:]]/, "", \$2); print \$2; exit}' "\$gm_conf")
|
|
[[ -n "\$val" ]] && gm_force="\$val"
|
|
done
|
|
if [[ -n "\$gm_force" ]]; then
|
|
export OUTPUT_CONNECTOR="\$gm_force"
|
|
log "Using forced connector: \$OUTPUT_CONNECTOR (FORCE_OUTPUT_CONNECTOR)"
|
|
elif [[ -x /usr/local/bin/gaming-pick-connector ]]; then
|
|
chosen=\$(/usr/local/bin/gaming-pick-connector 2>/dev/null)
|
|
if [[ -n "\$chosen" ]]; then
|
|
export OUTPUT_CONNECTOR="\$chosen"
|
|
log "Using connector: \$OUTPUT_CONNECTOR (lid-aware autopick)"
|
|
else
|
|
log "Warning: gaming-pick-connector found no connected display"
|
|
fi
|
|
fi
|
|
|
|
log "Starting gamescope-session-plus (gen=${INTEL_GPU_GEN:-unknown})"
|
|
|
|
/usr/share/gamescope-session-plus/gamescope-session-plus steam
|
|
rc=\$?
|
|
|
|
exit "\$rc"
|
|
NM_WRAPPER
|
|
sudo chmod +x "$nm_wrapper"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# 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
|
|
Comment=Steam Big Picture with gamescope-session
|
|
Exec=/usr/local/bin/gamescope-session-nm-wrapper
|
|
Type=Application
|
|
DesktopNames=gamescope
|
|
SESSION_DESKTOP
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Session Switch Scripts
|
|
#---------------------------------------------------------------------------
|
|
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 || true
|
|
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"
|
|
|
|
local switch_script="/usr/local/bin/switch-to-gaming"
|
|
sudo tee "$switch_script" > /dev/null << 'SWITCH_SCRIPT'
|
|
#!/bin/bash
|
|
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 gamescope 2>/dev/null || true
|
|
pkill -f gamescope-session 2>/dev/null || true
|
|
sleep 2
|
|
pkill -9 gamescope 2>/dev/null || true
|
|
pkill -9 -f gamescope-session 2>/dev/null || true
|
|
sudo -n chvt 2 2>/dev/null || true
|
|
sleep 0.3
|
|
sudo -n systemctl restart sddm
|
|
SWITCH_SCRIPT
|
|
sudo chmod +x "$switch_script"
|
|
|
|
local switch_desktop_script="/usr/local/bin/switch-to-desktop"
|
|
sudo tee "$switch_desktop_script" > /dev/null << 'SWITCH_DESKTOP'
|
|
#!/bin/bash
|
|
[[ ! -f /tmp/.gaming-session-active ]] && exit 0
|
|
rm -f /tmp/.gaming-session-active
|
|
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true
|
|
timeout 5 steam -shutdown 2>/dev/null || true
|
|
sleep 1
|
|
pkill gamescope 2>/dev/null || true
|
|
pkill -f gamescope-session 2>/dev/null || true
|
|
sleep 2
|
|
pkill -9 gamescope 2>/dev/null || true
|
|
pkill -9 -f gamescope-session 2>/dev/null || true
|
|
sudo -n chvt 2 2>/dev/null || true
|
|
sleep 0.3
|
|
nohup sudo -n systemctl restart sddm &>/dev/null &
|
|
disown
|
|
exit 0
|
|
SWITCH_DESKTOP
|
|
sudo chmod +x "$switch_desktop_script"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Keybind Monitor (Python)
|
|
#---------------------------------------------------------------------------
|
|
local keybind_monitor="/usr/local/bin/gaming-keybind-monitor"
|
|
sudo tee "$keybind_monitor" > /dev/null << 'KEYBIND_MONITOR'
|
|
#!/usr/bin/env python3
|
|
"""Gaming Mode Keybind Monitor - Super+Shift+R to exit"""
|
|
import sys, subprocess, time
|
|
try:
|
|
import evdev
|
|
from evdev import ecodes
|
|
except ImportError:
|
|
sys.exit(1)
|
|
|
|
def find_keyboards():
|
|
keyboards = []
|
|
for path in evdev.list_devices():
|
|
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 Exception:
|
|
continue
|
|
return keyboards
|
|
|
|
def monitor_keyboards(keyboards):
|
|
meta_pressed = shift_pressed = False
|
|
from selectors import DefaultSelector, EVENT_READ
|
|
selector = DefaultSelector()
|
|
for kbd in keyboards:
|
|
selector.register(kbd, EVENT_READ)
|
|
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:
|
|
subprocess.run(['/usr/local/bin/switch-to-desktop'])
|
|
return
|
|
except Exception:
|
|
continue
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
selector.close()
|
|
|
|
def main():
|
|
time.sleep(2)
|
|
keyboards = find_keyboards()
|
|
if keyboards:
|
|
monitor_keyboards(keyboards)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
KEYBIND_MONITOR
|
|
sudo chmod +x "$keybind_monitor"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Steam Library Auto-Mounter
|
|
#---------------------------------------------------------------------------
|
|
local steam_mount_script="/usr/local/bin/steam-library-mount"
|
|
sudo tee "$steam_mount_script" > /dev/null << 'STEAM_MOUNT'
|
|
#!/bin/bash
|
|
# Steam Library Drive Auto-Mounter
|
|
|
|
check_steam_library() {
|
|
local mount_point="$1"
|
|
[[ -d "$mount_point/steamapps" ]] || [[ -d "$mount_point/SteamLibrary/steamapps" ]] || \
|
|
[[ -f "$mount_point/libraryfolder.vdf" ]] || [[ -f "$mount_point/steamapps/libraryfolder.vdf" ]]
|
|
}
|
|
|
|
handle_device() {
|
|
local device="$1"
|
|
findmnt -n "$device" &>/dev/null && return
|
|
[[ "$device" =~ [0-9]$ ]] || 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) ;;
|
|
*) return ;;
|
|
esac
|
|
command -v udisksctl &>/dev/null || return
|
|
udisksctl mount -b "$device" --no-user-interaction 2>/dev/null || return
|
|
local mount_point
|
|
mount_point="$(findmnt -n -o TARGET "$device" 2>/dev/null)"
|
|
[[ -z "$mount_point" ]] && return
|
|
check_steam_library "$mount_point" || udisksctl unmount -b "$device" --no-user-interaction 2>/dev/null
|
|
}
|
|
|
|
shopt -s nullglob
|
|
for dev in /dev/sd*[0-9]* /dev/nvme*p[0-9]* /dev/mmcblk*p[0-9]*; do
|
|
[[ -b "$dev" ]] && handle_device "$dev"
|
|
done
|
|
shopt -u nullglob
|
|
|
|
action="" dev_path=""
|
|
udevadm monitor --kernel --property --subsystem-match=block 2>/dev/null | while read -r line; do
|
|
case "$line" in
|
|
ACTION=*) action="${line#ACTION=}" ;;
|
|
DEVNAME=*) dev_path="${line#DEVNAME=}" ;;
|
|
"")
|
|
if [[ "$action" == "add" && -n "$dev_path" && "$dev_path" =~ [0-9]$ && -b "$dev_path" ]]; then
|
|
sleep 1
|
|
handle_device "$dev_path"
|
|
fi
|
|
action="" dev_path=""
|
|
;;
|
|
esac
|
|
done
|
|
STEAM_MOUNT
|
|
sudo chmod +x "$steam_mount_script"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Output Connector Picker (lid-aware)
|
|
#
|
|
# Picks a DRM connector for gamescope at session start. Logic:
|
|
# lid closed -> prefer external (HDMI/DP), fall back to any connected
|
|
# lid open -> prefer internal (eDP/LVDS/DSI), fall back to external
|
|
# no lid -> any connected (desktop case)
|
|
# Lid state is read from /proc/acpi/button/lid/<id>/state when present.
|
|
# Connector status from /sys/class/drm/card*-*/status (Omarchy-style).
|
|
#---------------------------------------------------------------------------
|
|
local pick_connector_script="/usr/local/bin/gaming-pick-connector"
|
|
sudo tee "$pick_connector_script" > /dev/null << 'PICK_CONNECTOR'
|
|
#!/bin/bash
|
|
# gaming-pick-connector — print the best DRM connector for gamescope.
|
|
# Scoped to the xe-bound DRM card (matches the GPU the installer selected).
|
|
# Empty output means no connected display was found.
|
|
|
|
set -u
|
|
|
|
is_internal() {
|
|
case "$1" in
|
|
eDP-*|eDP*|LVDS-*|LVDS*|DSI-*|DSI*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Find the xe-bound card. If multiple are xe (e.g. Battlemage dGPU + Xe2/Xe3
|
|
# iGPU), prefer the dGPU (PCI bus != 00) — matches find_intel_gpu().
|
|
pick_xe_card() {
|
|
local best_card="" best_is_dgpu=-1
|
|
for d in /sys/class/drm/card[0-9]*; do
|
|
local n; n=$(basename "$d")
|
|
[[ "$n" == render* ]] && continue
|
|
[[ -L "$d/device/driver" ]] || continue
|
|
local drv; drv=$(basename "$(readlink "$d/device/driver")")
|
|
[[ "$drv" == "xe" ]] || continue
|
|
local pci; pci=$(basename "$(readlink -f "$d/device")")
|
|
local is_dgpu=1
|
|
[[ "$pci" =~ ^0000:00: ]] && is_dgpu=0
|
|
if (( is_dgpu > best_is_dgpu )); then
|
|
best_card="$n"
|
|
best_is_dgpu=$is_dgpu
|
|
fi
|
|
done
|
|
echo "$best_card"
|
|
}
|
|
|
|
xe_card=$(pick_xe_card)
|
|
# Fallback: if no xe card found (shouldn't happen post-install), scan all
|
|
glob_root="${xe_card:-card*}"
|
|
|
|
# Lid state: open / closed / unknown
|
|
lid_state="unknown"
|
|
for f in /proc/acpi/button/lid/*/state; do
|
|
[[ -r "$f" ]] || continue
|
|
case "$(awk '{print $NF}' "$f" 2>/dev/null)" in
|
|
closed) lid_state="closed" ;;
|
|
open) lid_state="open" ;;
|
|
esac
|
|
break
|
|
done
|
|
|
|
# Collect connected connectors on the chosen card only
|
|
connected=()
|
|
for status in /sys/class/drm/${glob_root}-*/status; do
|
|
[[ -r "$status" ]] || continue
|
|
[[ "$(<"$status")" == "connected" ]] || continue
|
|
name="$(basename "${status%/status}")"
|
|
name="${name#card*-}"
|
|
connected+=("$name")
|
|
done
|
|
|
|
(( ${#connected[@]} == 0 )) && exit 0
|
|
|
|
# Split into internal vs external preserving order
|
|
internal=() external=()
|
|
for c in "${connected[@]}"; do
|
|
if is_internal "$c"; then internal+=("$c"); else external+=("$c"); fi
|
|
done
|
|
|
|
# Pick by lid state
|
|
if [[ "$lid_state" == "closed" ]]; then
|
|
if (( ${#external[@]} > 0 )); then echo "${external[0]}"; exit 0; fi
|
|
(( ${#internal[@]} > 0 )) && echo "${internal[0]}"; exit 0
|
|
fi
|
|
# lid open or unknown (desktop): prefer internal, fall back to external
|
|
if (( ${#internal[@]} > 0 )); then echo "${internal[0]}"; exit 0; fi
|
|
(( ${#external[@]} > 0 )) && echo "${external[0]}"
|
|
PICK_CONNECTOR
|
|
sudo chmod +x "$pick_connector_script"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# SDDM Configuration
|
|
#---------------------------------------------------------------------------
|
|
sudo mkdir -p /etc/sddm.conf.d
|
|
|
|
local sddm_gaming_conf="/etc/sddm.conf.d/zz-gaming-session.conf"
|
|
local autologin_user="$current_user"
|
|
[[ -f /etc/sddm.conf.d/autologin.conf ]] && \
|
|
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"
|
|
|
|
sudo tee "$sddm_gaming_conf" > /dev/null << SDDM_GAMING
|
|
[Autologin]
|
|
User=${autologin_user}
|
|
Session=hyprland-uwsm
|
|
Relogin=true
|
|
SDDM_GAMING
|
|
|
|
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"
|
|
[[ ! -f "$CONF" ]] && exit 1
|
|
case "$1" in
|
|
gaming) sed -i 's/^Session=.*/Session=gamescope-session-steam-nm/' "$CONF" ;;
|
|
desktop) sed -i 's/^Session=.*/Session=hyprland-uwsm/' "$CONF" ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
SESSION_HELPER
|
|
sudo chmod +x "$session_helper"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Sudoers Rules
|
|
#---------------------------------------------------------------------------
|
|
local sudoers_session="/etc/sudoers.d/gaming-session-switch"
|
|
sudo tee "$sudoers_session" > /dev/null << 'SUDOERS_SWITCH'
|
|
%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
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl start NetworkManager.service
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop NetworkManager.service
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart iwd.service
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart bluetooth.service
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-start
|
|
%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-stop
|
|
SUDOERS_SWITCH
|
|
sudo chmod 0440 "$sudoers_session"
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Hyprland Keybind
|
|
#
|
|
# Combo: SUPER SHIFT, S. The screenshot template at this combo is
|
|
# commented out by default in Omarchy's bindings.conf (Print Screen key
|
|
# handles screenshots), so the combo is free in practice. SUPER SHIFT, G
|
|
# is taken by Signal in Omarchy.
|
|
#
|
|
# Exec is wrapped with `uwsm-app --` on Omarchy so the helper runs under
|
|
# the user's uwsm graphical session like every other Omarchy launcher.
|
|
#---------------------------------------------------------------------------
|
|
local hypr_bindings_conf="${user_home}/.config/hypr/bindings.conf"
|
|
local hypr_main_conf="${user_home}/.config/hypr/hyprland.conf"
|
|
local keybind_target=""
|
|
|
|
if [[ -f "$hypr_bindings_conf" ]]; then
|
|
keybind_target="$hypr_bindings_conf"
|
|
elif [[ -f "$hypr_main_conf" ]]; then
|
|
keybind_target="$hypr_main_conf"
|
|
fi
|
|
|
|
# Build the keybind line — uwsm-app wrapping if available (Omarchy convention)
|
|
local keybind_exec="/usr/local/bin/switch-to-gaming"
|
|
if command -v uwsm-app >/dev/null 2>&1; then
|
|
keybind_exec="uwsm-app -- /usr/local/bin/switch-to-gaming"
|
|
fi
|
|
local keybind_line="bindd = SUPER SHIFT, S, Gaming Mode, exec, ${keybind_exec}"
|
|
|
|
# Collision pre-check across all sourced binding files (Omarchy + user).
|
|
# Active (uncommented) lines only — Omarchy's screenshot template at this
|
|
# combo is commented out by default and doesn't count as a collision.
|
|
local -a binding_sources=()
|
|
[[ -d "${user_home}/.local/share/omarchy/default/hypr/bindings" ]] && \
|
|
while IFS= read -r f; do binding_sources+=("$f"); done < <(find "${user_home}/.local/share/omarchy/default/hypr/bindings" -name '*.conf' 2>/dev/null)
|
|
[[ -f "$hypr_bindings_conf" ]] && binding_sources+=("$hypr_bindings_conf")
|
|
[[ -f "$hypr_main_conf" ]] && binding_sources+=("$hypr_main_conf")
|
|
local collision=""
|
|
if ((${#binding_sources[@]})); then
|
|
collision=$(grep -hE '^bindd? = SUPER SHIFT, S,' "${binding_sources[@]}" 2>/dev/null | head -1)
|
|
fi
|
|
if [[ -n "$collision" && "$collision" != *"switch-to-gaming"* ]]; then
|
|
warn "SUPER SHIFT, S is already bound: ${collision}"
|
|
warn "Skipping keybind install. Add manually with a free combo:"
|
|
warn " ${keybind_line}"
|
|
elif [[ -n "$keybind_target" ]] && ! grep -q "switch-to-gaming" "$keybind_target" 2>/dev/null; then
|
|
run_as_user tee -a "$keybind_target" > /dev/null << HYPR_GAMING
|
|
|
|
# Gaming Mode - Switch to Gamescope session (Intel Arc)
|
|
${keybind_line}
|
|
HYPR_GAMING
|
|
info "Added Gaming Mode keybind to $(basename "$keybind_target")"
|
|
elif [[ -z "$keybind_target" ]]; then
|
|
warn "No Hyprland config found - please add keybind manually:"
|
|
warn " ${keybind_line}"
|
|
fi
|
|
|
|
# Reload Hyprland — prefer omarchy-restart-hyprctl on Omarchy
|
|
if is_omarchy && command -v omarchy-restart-hyprctl >/dev/null 2>&1; then
|
|
run_as_user omarchy-restart-hyprctl >/dev/null 2>&1 || true
|
|
elif command -v hyprctl >/dev/null 2>&1 && hyprctl monitors >/dev/null 2>&1; then
|
|
hyprctl reload >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Done
|
|
#---------------------------------------------------------------------------
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SESSION SWITCHING CONFIGURED (Intel Arc)"
|
|
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 ""
|
|
}
|
|
|
|
###############################################################################
|
|
# MAIN ENTRY POINT
|
|
###############################################################################
|
|
|
|
execute_setup() {
|
|
sudo -k
|
|
sudo -v || die "sudo authentication required"
|
|
|
|
validate_environment
|
|
check_intel_gpu
|
|
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " ARCGames INSTALLER v${ARCGAMES_VERSION}"
|
|
echo " Intel Arc Gaming Mode Setup (dGPU + Xe2/Xe3 iGPU)"
|
|
echo "================================================================"
|
|
echo ""
|
|
|
|
check_steam_dependencies
|
|
bootstrap_steam_login
|
|
setup_requirements
|
|
setup_session_switching
|
|
|
|
if [[ "$NEEDS_RELOGIN" -eq 1 ]]; then
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " IMPORTANT: LOG OUT REQUIRED"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " User groups have been updated. Please log out and log back in."
|
|
echo ""
|
|
else
|
|
echo ""
|
|
echo "================================================================"
|
|
echo " SETUP COMPLETE"
|
|
echo "================================================================"
|
|
echo ""
|
|
echo " To switch to Gaming Mode: Press Super+Shift+S"
|
|
echo " To return to Desktop: Press Super+Shift+R"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
ARCGames Installer v${ARCGAMES_VERSION}
|
|
|
|
Gaming Mode installer for Intel Arc GPUs (dGPU and Xe2/Xe3 iGPU).
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Options:
|
|
--help, -h Show this help message
|
|
--version Show version number
|
|
|
|
EOF
|
|
}
|
|
|
|
|
|
###############################################################################
|
|
# COMMAND LINE HANDLING
|
|
###############################################################################
|
|
|
|
case "${1:-}" in
|
|
--help|-h) show_help; exit 0 ;;
|
|
--version) echo "ARCGames Installer v${ARCGAMES_VERSION}"; exit 0 ;;
|
|
"") execute_setup ;;
|
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
esac
|