From 82cc86583afb3fd05185ebe7ec323f80fc113c2a Mon Sep 17 00:00:00 2001 From: nosignal Date: Mon, 20 Apr 2026 20:07:57 +0100 Subject: [PATCH] Initial commit: Super Shift G Mint Deck Mode v13.00-mint Linux Mint Cinnamon gaming mode installer. Super+Shift+G switches to a Gamescope + Steam Big Picture session via LightDM, with performance tuning, external drive auto-mount, and AMD/NVIDIA support. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 320 +++++ super_shift_g_mint.sh | 3163 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 3483 insertions(+) create mode 100644 README.md create mode 100755 super_shift_g_mint.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..68abb56 --- /dev/null +++ b/README.md @@ -0,0 +1,320 @@ +# Super Shift G - Mint Deck Mode + +**Version 13.00-mint** + +Turn a Linux Mint Cinnamon desktop into a Steam Deck-like gaming console with a single keybind. Press `Super+Shift+G` to enter Gaming Mode (Steam Big Picture in Gamescope), and use Steam's "Exit to Desktop" to return to Cinnamon. + +Built for [Linux Mint](https://linuxmint.com/) running Cinnamon on LightDM. + +## What It Does + +This installer transforms your desktop into a dual-mode system: + +- **Desktop Mode** - Your normal Cinnamon (X11) session +- **Gaming Mode** - Full-screen Steam Big Picture running inside Gamescope (the same compositor used by the Steam Deck), with automatic performance tuning, controller support, and external drive mounting + +Switching between modes is seamless — LightDM handles session transitions, and all your network, audio, and peripherals carry over automatically. + +## Requirements + +- **OS**: [Linux Mint](https://linuxmint.com/) (Ubuntu-based, tested on Mint 22) +- **Desktop**: Cinnamon (X11) +- **Display Manager**: LightDM +- **GPU**: AMD (discrete or APU) or NVIDIA (discrete) + - Intel-only systems are **not supported** + - Intel iGPU + AMD/NVIDIA dGPU configurations work fine + +> **Note**: This script is designed specifically for Linux Mint and its stack (Cinnamon, LightDM, NetworkManager, PipeWire). It uses `apt` for package management and builds Gamescope from source as a recent build is not available in Mint repos. It is not intended for Arch-based or Fedora-based distributions — see [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/nosignal/Super-Shift-S-Omarchy-Deck-Mode) for Omarchy/Arch, or [Super-Shift-G-Nobara-Deck-Mode](https://git.no-signal.uk/nosignal/Super-Shift-G-Nobara-Deck-Mode) for Nobara/Fedora. + +## Quick Start + +```bash +git clone https://git.no-signal.uk/nosignal/Super-Shift-G-Mint-Deck-Mode.git +cd Super-Shift-G-Mint-Deck-Mode +chmod +x super_shift_g_mint.sh +./super_shift_g_mint.sh +``` + +The installer is fully interactive and will walk you through each step. + +## Usage + +| Action | Keybind | +|--------|---------| +| Enter Gaming Mode | `Super + Shift + G` | +| Return to Desktop | Steam > Power > Exit to Desktop | + +### Command-Line Options + +``` +./super_shift_g_mint.sh # Full installation +./super_shift_g_mint.sh --verify # Verify installation only +./super_shift_g_mint.sh --version # Show version +./super_shift_g_mint.sh --help # Show help +``` + +## What Gets Installed + +### Packages (via apt) + +The installer checks for and offers to install: + +**Core Steam Dependencies** +- `steam-installer`, `gamescope` (built from source), `mangohud` (built from source), `gamemode` +- Vulkan loaders and Mesa libraries (32-bit and 64-bit) +- Audio libraries (PipeWire / pulseaudio compatibility) +- Fonts (`fonts-liberation`) + +**GPU-Specific Drivers** +- **NVIDIA**: proprietary driver packages, `libnvidia-egl-wayland1`, VA-API driver +- **AMD**: `libvulkan1`, `mesa-vulkan-drivers`, `libva-mesa-driver` + +**Build Dependencies** (for Gamescope from source) +- Meson, ninja, CMake, `libwayland-dev`, `libdrm-dev`, and others + +**ChimeraOS Session Scripts** +- Cloned from GitHub — provides the gamescope-session framework + +**Apt Resilience** +- If a package install hits a broken dpkg state or mirror failure, the installer automatically cleans the cache, retries, and falls back through multiple Ubuntu mirrors before failing. + +### Files Created + +#### Session Scripts +| Path | Purpose | +|------|---------| +| `/usr/local/bin/switch-to-gaming` | Switches from Cinnamon to Gaming Mode | +| `/usr/local/bin/switch-to-desktop` | Switches from Gaming Mode back to Cinnamon | +| `/usr/local/bin/gamescope-session-nm-wrapper` | Main session wrapper (performance mode, NM, drive mounting) | +| `/usr/local/bin/gaming-session-switch` | Helper to toggle LightDM session config between modes | +| `/usr/local/bin/gaming-keybind-monitor` | Python daemon monitoring keyboard in Gaming Mode | +| `/usr/lib/os-session-select` | Handler for Steam's "Exit to Desktop" button | +| `/usr/local/lib/gamescope-nvidia/gamescope` | NVIDIA wrapper adding `--force-composition` flag | + +#### NetworkManager Integration +| Path | Purpose | +|------|---------| +| `/usr/local/bin/gamescope-nm-start` | Ensures NetworkManager is active on gaming session entry | +| `/usr/local/bin/gamescope-nm-stop` | No-op on Mint (NetworkManager always runs) — kept for compatibility | + +#### External Drive Support +| Path | Purpose | +|------|---------| +| `/usr/local/bin/steam-library-mount` | Auto-detects and mounts drives with Steam libraries | + +#### Session & Display Manager +| Path | Purpose | +|------|---------| +| `/usr/share/xsessions/gamescope-session-steam-nm.desktop` | LightDM session entry for Gaming Mode | + +#### Permissions & Security +| Path | Purpose | +|------|---------| +| `/etc/sudoers.d/gaming-session-switch` | Passwordless sudo for session switching, NM, bluetooth | +| `/etc/sudoers.d/gaming-mode-sysctl` | Passwordless sudo for performance sysctl tuning | +| `/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules` | Polkit rules for NM D-Bus access | +| `/etc/polkit-1/rules.d/50-udisks-gaming.rules` | Polkit rules for external drive mounting | +| `/etc/udev/rules.d/99-gaming-performance.rules` | Udev rules for CPU/GPU performance control | +| `/etc/security/limits.d/99-gaming-memlock.conf` | Memory lock limits (2GB) for gaming | + +#### Performance & Environment +| Path | Purpose | +|------|---------| +| `/etc/environment.d/99-shader-cache.conf` | Shader cache optimisation (12GB Mesa/DXVK cache) | +| `/etc/environment.d/90-nvidia-gamescope.conf` | NVIDIA Gamescope environment variables | +| `/etc/pipewire/pipewire.conf.d/10-gaming-latency.conf` | PipeWire low-latency audio config | + +#### User Config +| Path | Purpose | +|------|---------| +| `~/.config/environment.d/gamescope-session-plus.conf` | Gamescope session config (resolution, refresh rate, GPU) | + +## How It Works + +### Session Switching Flow + +``` +Desktop Mode (Cinnamon) + | + +- Super+Shift+G pressed + | +- switch-to-gaming runs: + | +- Masks suspend targets (prevents sleep during switch) + | +- Updates LightDM config to gaming session + | +- Restarts LightDM -> boots into Gaming Mode + | +Gaming Mode (Gamescope + Steam Big Picture) + | + +- On session start (gamescope-session-nm-wrapper): + | +- Enables performance mode (CPU governor, GPU tuning) + | +- Ensures NetworkManager is active (Mint never stops NM) + | +- Launches steam-library-mount (external drive detection) + | +- Starts gaming-keybind-monitor + | +- Launches gamescope-session-plus with Steam + | + +- Steam > Power > Exit to Desktop + | +- switch-to-desktop runs: + | +- Unmasks suspend targets + | +- Restores Bluetooth + | +- Shuts down Steam gracefully + | +- Kills gamescope (releases DRM master — critical for AMD) + | +- Updates LightDM config to Cinnamon session + | +- Restarts LightDM -> boots into Desktop Mode + | + +- On session cleanup (trap handler): + +- Kills steam-library-mount and keybind-monitor + +- Restores balanced power mode +``` + +### Performance Mode + +When Gaming Mode starts, the session wrapper automatically: + +- Sets CPU governor to `performance` on all cores +- **NVIDIA**: Enables persistence mode, sets power limit to maximum, disables runtime suspend +- **AMD**: Sets GPU to high performance via `power_dpm_force_performance_level` +- Applies kernel sysctl tuning (scheduler, VM, inotify, network buffers) +- Sets power profile to `performance` (if power-profiles-daemon is available) + +On exit, everything is restored to balanced/powersave defaults. + +### GPU Detection + +The installer automatically detects your GPU configuration: + +- **AMD dGPU**: Detected via PCI device names (Navi, RDNA, Vega discrete cards) +- **AMD APU**: Detected via integrated GPU codenames (Phoenix, Rembrandt, Van Gogh, etc.) +- **NVIDIA**: Detected via lspci, configures `nvidia-drm.modeset=1` if missing +- **Multi-GPU**: Correctly identifies discrete vs integrated, selects dGPU for gaming + +### Monitor Detection + +The installer scans DRM connectors on your gaming GPU to find connected displays. If multiple monitors are connected to the dGPU, you can choose which one to use for Gaming Mode. Resolution and refresh rate are auto-detected from EDID data. + +### External Drive Auto-Mount + +The `steam-library-mount` daemon runs during Gaming Mode and: + +1. Scans all connected drives for Steam library folders +2. Mounts drives containing `steamapps/` directories via udisks2 +3. Monitors udev for hot-plugged drives +4. Unmounts non-Steam drives to avoid clutter + +Supports ext4, NTFS, btrfs, xfs, exfat, f2fs, and vfat filesystems. + +## Configuration + +### Config File + +The installer reads from `/etc/gaming-mode.conf` (or `~/.gaming-mode.conf` if it exists): + +```bash +PERFORMANCE_MODE=enabled # Set to "disabled" to skip performance tuning +``` + +### Gamescope Session Config + +After installation, you can edit `~/.config/environment.d/gamescope-session-plus.conf`: + +```bash +SCREEN_WIDTH=2560 +SCREEN_HEIGHT=1440 +CUSTOM_REFRESH_RATES=165 +OUTPUT_CONNECTOR=DP-1 +ADAPTIVE_SYNC=1 # AMD only +ENABLE_GAMESCOPE_HDR=1 # AMD only +``` + +**NVIDIA note**: Resolution is capped at 2560x1440 due to Gamescope limitations with NVIDIA GPUs. + +### Shader Cache + +The installer configures a 12GB shader cache by default in `/etc/environment.d/99-shader-cache.conf`. This reduces stutter in games by caching compiled shaders. Values can be adjusted: + +```bash +MESA_SHADER_CACHE_MAX_SIZE=12G +__GL_SHADER_DISK_CACHE_SIZE=12884901888 +DXVK_STATE_CACHE=1 +``` + +## Key Differences from Other Versions + +| | Omarchy (Arch) | Nobara (Fedora) | **Mint (Ubuntu)** | +|---|---|---|---| +| **Package manager** | pacman / yay | dnf | **apt** | +| **Desktop** | Hyprland | KDE Plasma | **Cinnamon (X11)** | +| **Display manager** | SDDM | plasmalogin | **LightDM** | +| **Networking** | iwd | NetworkManager | **NetworkManager (never stopped)** | +| **Gamescope** | pacman package | Built from source | **Built from source** | +| **MangoHud** | pacman package | dnf package | **Built from source** | +| **ChimeraOS session** | AUR packages | Cloned from GitHub | **Cloned from GitHub** | +| **Gaming keybind** | `Super+Shift+S` | `Super+Alt+G` | **`Super+Shift+G`** | +| **Return keybind** | `Super+Shift+R` | Steam > Exit to Desktop | **Steam > Exit to Desktop** | + +## NVIDIA-Specific Notes + +- **Kernel parameter**: `nvidia-drm.modeset=1` is required. The installer can configure this for GRUB. +- **Resolution cap**: Gamescope on NVIDIA is limited to 2560x1440 maximum. +- **Force composition**: The NVIDIA wrapper automatically adds `--force-composition` if supported by your Gamescope version. +- **Environment**: `GBM_BACKEND=nvidia-drm` and related vars are set automatically. +- **Persistence mode**: Enabled during gaming to keep the GPU initialised, disabled on exit. + +## Troubleshooting + +### Verify Installation + +Run the built-in verification to check all files, permissions, packages, and services: + +```bash +./super_shift_g_mint.sh --verify +``` + +### Common Issues + +**Gaming Mode doesn't start / black screen** +- Check NVIDIA kernel params: `cat /proc/cmdline | grep nvidia` +- Verify gamescope works: `gamescope -- steam` +- Check session logs: `journalctl --user -u gamescope-session -n 50` + +**Apt install failures** +- The installer retries with fallback mirrors automatically +- Manual recovery: `sudo dpkg --configure -a && sudo apt clean && sudo apt install -y --fix-broken` + +**Super+Shift+G doesn't work** +- Log out and back in after installation +- Check Cinnamon keyboard shortcuts for "Switch to Gaming Mode" +- Verify the desktop file exists: `ls /usr/share/applications/switch-to-gaming.desktop` + +**External drives not mounting** +- Ensure `udisks2` is installed: `dpkg -l udisks2` +- Check polkit rules exist: `ls /etc/polkit-1/rules.d/50-udisks-gaming.rules` +- Check mount logs: `journalctl -t steam-library-mount -n 20` + +**Audio stuttering in Gaming Mode** +- Check PipeWire config exists: `cat /etc/pipewire/pipewire.conf.d/10-gaming-latency.conf` +- Try lower quantum: edit the config and set `default.clock.min-quantum = 128` + +**"NO DICE - INTEL ONLY DETECTED"** +- This system has only Intel graphics. Gaming Mode requires AMD or NVIDIA. +- If you have a discrete GPU, check that its driver is loaded: `lspci -k | grep -A2 VGA` + +### Log Locations + +| Component | Command | +|-----------|---------| +| Gaming session | `journalctl --user -u gamescope-session` | +| NetworkManager | `journalctl -t gamescope-nm` | +| Drive mounting | `journalctl -t steam-library-mount` | +| Keybind monitor | `journalctl -t gaming-keybind-monitor` | +| Session wrapper | `journalctl -t gamescope-wrapper` | +| Installation | `journalctl -t gaming-mode` | + +## Credits + +- [Linux Mint](https://linuxmint.com/) - The Ubuntu-based distribution this was adapted for +- [Omarchy](https://omarchy.com) - Original script built for Omarchy (Arch Linux) +- [ChimeraOS](https://chimeraos.org/) - gamescope-session packages +- [Valve](https://store.steampowered.com/) - Steam, Gamescope, and the Steam Deck inspiration + +## License + +This project is provided as-is for the Linux Mint gaming community. diff --git a/super_shift_g_mint.sh b/super_shift_g_mint.sh new file mode 100755 index 0000000..6977bed --- /dev/null +++ b/super_shift_g_mint.sh @@ -0,0 +1,3163 @@ +#!/bin/bash +set -Euo pipefail + +Super_Shift_S_VERSION="13.00-mint" + +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}" + +NEEDS_RELOGIN=0 +NEEDS_REBOOT=0 + +info(){ echo "[*] $*"; } +warn(){ echo "[!] $*"; } +err(){ echo "[!] $*" >&2; } + +die() { + local msg="$1"; local code="${2:-1}" + echo "FATAL: $msg" >&2 + logger -t gaming-mode "Installation failed: $msg" + exit "$code" +} + +FALLBACK_MIRRORS=( + "http://us.archive.ubuntu.com/ubuntu" + "http://mirror.csclub.uwaterloo.ca/ubuntu" + "http://mirrors.kernel.org/ubuntu" +) + +apt_fix_and_retry() { + # Attempt to recover from broken dpkg state and mirror failures, then retry install. + # Usage: apt_fix_and_retry pkg1 pkg2 ... + local -a packages=("$@") + local attempt=0 + local max_attempts=${#FALLBACK_MIRRORS[@]} + + # Step 1: Fix any broken dpkg state from the failed install + info "Fixing broken package state..." + sudo dpkg --configure -a 2>/dev/null || true + sudo apt install -y --fix-broken 2>/dev/null || true + + # Step 2: Clean cache (corrupted downloads cause 400 errors on retry) + info "Cleaning apt cache..." + sudo apt clean + + # Step 3: Retry with current mirrors first + info "Retrying package install..." + sudo apt update 2>/dev/null || true + if sudo apt install -y --fix-missing "${packages[@]}" 2>/dev/null; then + info "Packages installed successfully on retry" + return 0 + fi + + # Step 4: Try fallback mirrors one by one + for mirror in "${FALLBACK_MIRRORS[@]}"; do + ((attempt++)) + warn "Attempt $attempt/$max_attempts: Switching to mirror $mirror" + + # Replace archive.ubuntu.com (and any previous fallback) with new mirror + sudo sed -i "s|http://[^ ]*/ubuntu|${mirror}|g" /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null || true + + sudo dpkg --configure -a 2>/dev/null || true + sudo apt clean + sudo apt update 2>/dev/null || true + sudo apt install -y --fix-broken 2>/dev/null || true + + if sudo apt install -y --fix-missing "${packages[@]}" 2>/dev/null; then + info "Packages installed successfully using mirror: $mirror" + return 0 + fi + done + + # Step 5: Last resort - install packages one at a time to identify the problem + warn "Bulk install failed - trying packages individually..." + local -a still_missing=() + for pkg in "${packages[@]}"; do + if ! check_package "$pkg"; then + if ! sudo apt install -y --fix-missing "$pkg" 2>/dev/null; then + still_missing+=("$pkg") + fi + fi + done + + if ((${#still_missing[@]})); then + err "Failed to install ${#still_missing[@]} package(s) after all retry attempts:" + for pkg in "${still_missing[@]}"; do + echo " - $pkg" + done + return 1 + fi + + info "All packages installed (individually)" + return 0 +} + +validate_environment() { + command -v apt >/dev/null || die "apt required (not a Debian/Ubuntu-based system)" + + # Check for Cinnamon or X11 session + local de_ok=false + if [[ "${XDG_CURRENT_DESKTOP:-}" == *"Cinnamon"* ]] || \ + [[ "${DESKTOP_SESSION:-}" == *"cinnamon"* ]] || \ + [[ "${XDG_SESSION_TYPE:-}" == "x11" ]] || \ + pgrep -x cinnamon >/dev/null 2>&1; then + de_ok=true + fi + if [[ "$de_ok" != "true" ]]; then + warn "Cinnamon/X11 session not detected (XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-unset})" + warn "This script is designed for Linux Mint with Cinnamon desktop" + read -p "Continue anyway? [y/N]: " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] || die "Aborting - Cinnamon desktop not detected" + fi + + # Check LightDM + if ! command -v lightdm >/dev/null 2>&1 && ! systemctl is-active --quiet lightdm; then + warn "LightDM not detected - session switching requires LightDM" + read -p "Continue anyway? [y/N]: " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] || die "Aborting - LightDM not detected" + fi +} + +check_package() { dpkg -s "$1" &>/dev/null; } + +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 +} + +check_intel_only() { + 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 + + if $has_intel && ! $has_amd_nvidia; then + return 0 + fi + return 1 +} + +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 +} + +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 "" + + if [ -f /etc/default/grub ]; then + 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 + else + warn "Could not find /etc/default/grub" + show_manual_nvidia_instructions + fi +} + +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 update-grub || { + 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: + GRUB: Add nvidia-drm.modeset=1 to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub + Then run: sudo update-grub +MSG + warn "Gaming Mode may not work correctly without nvidia-drm.modeset=1" +} + +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 +} + +check_steam_dependencies() { + info "Checking Steam dependencies for Linux Mint..." + + info "Updating package database..." + if ! sudo apt update; then + warn "apt update had errors - cleaning cache and retrying..." + sudo apt clean + sudo dpkg --configure -a 2>/dev/null || true + sudo apt update || warn "apt update still has errors - continuing with available data" + fi + + 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..." + if ! sudo apt upgrade -y; then + warn "Upgrade failed - attempting to fix broken packages first..." + sudo dpkg --configure -a 2>/dev/null || true + sudo apt install -y --fix-broken 2>/dev/null || true + sudo apt clean + sudo apt update 2>/dev/null || true + if ! sudo apt upgrade -y 2>/dev/null; then + warn "Upgrade still failing - continuing with package installation" + warn "Broken packages will be resolved during dependency install" + fi + fi + fi + echo "" + + local -a missing_deps=() + local -a optional_deps=() + + if ! command -v lspci >/dev/null 2>&1; then + info "Installing pciutils for GPU detection..." + sudo apt install -y pciutils || { apt_fix_and_retry pciutils || die "Failed to install pciutils"; } + fi + + # Enable i386 architecture if not already enabled + if ! dpkg --print-foreign-architectures 2>/dev/null | grep -q i386; then + info "Enabling i386 architecture for 32-bit libraries..." + sudo dpkg --add-architecture i386 + sudo apt update || warn "apt update had errors after adding i386 - continuing" + else + info "i386 architecture: enabled" + fi + + local -a core_deps=( + "steam" + "libvulkan1" + "libvulkan1:i386" + "libgl1-mesa-dri" + "libgl1-mesa-dri:i386" + "mesa-utils" + "libc6:i386" + "libgcc-s1:i386" + "libx11-6:i386" + "libxss1:i386" + "libasound2-plugins:i386" + "libpulse0:i386" + "libopenal1:i386" + "libnss3:i386" + "libcups2t64:i386" + "libsdl2-2.0-0:i386" + "libfreetype6:i386" + "libfontconfig1:i386" + "libnm0:i386" + "network-manager" + "gamemode" + "libgamemode0:i386" + "fonts-liberation" + "xdg-user-dirs" + ) + + 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 + # Detect installed NVIDIA driver version for matching packages + local nvidia_ver="" + nvidia_ver=$(dpkg -l 'nvidia-driver-*' 2>/dev/null | awk '/^ii/{print $2}' | grep -oP '\d+' | head -1) + if [[ -z "$nvidia_ver" ]]; then + nvidia_ver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1 | cut -d. -f1) + fi + [[ -z "$nvidia_ver" ]] && nvidia_ver="590" + + info "NVIDIA driver version detected: $nvidia_ver" + + # Check if main NVIDIA packages are already installed + if ! check_package "nvidia-utils-${nvidia_ver}"; then + gpu_deps+=("nvidia-utils-${nvidia_ver}") + fi + if ! check_package "libnvidia-gl-${nvidia_ver}:i386"; then + gpu_deps+=("libnvidia-gl-${nvidia_ver}:i386") + fi + if ! check_package "nvidia-settings"; then + gpu_deps+=("nvidia-settings") + fi + fi + + if $has_amd; then + gpu_deps+=( + "mesa-vulkan-drivers" + "mesa-vulkan-drivers:i386" + "libvdpau1" + "libvdpau1:i386" + ) + fi + + if ! $has_nvidia && ! $has_amd; then + info "No NVIDIA/AMD GPU detected; installing AMD Vulkan drivers as fallback..." + gpu_deps+=("mesa-vulkan-drivers" "mesa-vulkan-drivers:i386") + fi + + gpu_deps+=( + "vulkan-tools" + "mesa-vulkan-drivers" + ) + + local -a recommended_deps=( + "mangohud" + "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 "" + + # Remove duplicates from missing_deps + local -a clean_missing=() + local -A seen_deps=() + for item in "${missing_deps[@]}"; do + if [[ -n "$item" && -z "${seen_deps[$item]:-}" ]]; then + clean_missing+=("$item") + seen_deps[$item]=1 + fi + 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..." + if ! sudo apt install -y "${missing_deps[@]}"; then + warn "Initial install failed - attempting recovery..." + if ! apt_fix_and_retry "${missing_deps[@]}"; then + # Check what's actually still missing after retries + local -a final_missing=() + for dep in "${missing_deps[@]}"; do + check_package "$dep" || final_missing+=("$dep") + done + if ((${#final_missing[@]})); then + err "Could not install ${#final_missing[@]} required package(s):" + for dep in "${final_missing[@]}"; do + echo " - $dep" + done + echo "" + echo " Possible fixes:" + echo " 1. Check your internet connection" + echo " 2. Try: sudo apt clean && sudo apt update" + echo " 3. Switch mirrors in Software Sources (mintSources)" + echo " 4. Run: sudo dpkg --configure -a && sudo apt install -y --fix-broken" + echo "" + read -p "Continue anyway with partial install? [y/N]: " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] || die "Missing required Steam dependencies" + warn "Continuing with partial install - some features may not work" + else + info "All required packages eventually installed (after recovery)" + fi + fi + else + info "Required dependencies installed successfully" + fi + 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 "Installing recommended packages..." + if ! sudo apt install -y "${optional_deps[@]}" 2>/dev/null; then + warn "Some recommended packages failed - attempting recovery..." + apt_fix_and_retry "${optional_deps[@]}" || warn "Some recommended packages could not be installed" + fi + fi + else + info "All recommended packages are already installed!" + fi + + echo "" + echo "================================================================" + + check_steam_config +} + +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 'sudo'; then + missing_groups+=("sudo") + 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" ;; + sudo) echo " - sudo - 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 sudo 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 + + # Check if Steam client is actually bootstrapped (apt only installs the bootstrapper) + if check_package "steam" || check_package "steam-installer"; then + local steam_bin="" + if [[ -x /usr/games/steam ]]; then + steam_bin="/usr/games/steam" + elif command -v steam >/dev/null 2>&1; then + steam_bin="$(command -v steam)" + fi + + if [[ -n "$steam_bin" ]] && [[ ! -f "$HOME/.local/share/Steam/ubuntu12_32/steam" ]]; then + echo "" + echo "================================================================" + echo " STEAM CLIENT BOOTSTRAP REQUIRED" + echo "================================================================" + echo "" + echo " The Steam package is installed, but the Steam client has not" + echo " completed its first-run download (~400MB)." + echo "" + echo " Steam will now launch to download and install the client." + echo " Once the login screen appears, you can close it or log in." + echo "" + read -p "Bootstrap Steam client now? [Y/n]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + info "Launching Steam to bootstrap client (this may take a few minutes)..." + # Run Steam in the foreground so user can see progress and log in + $steam_bin & + local steam_pid=$! + info "Steam launched (PID: $steam_pid) - waiting for bootstrap to complete..." + + # Wait for the Steam binary to appear (indicates bootstrap is done) + local wait_count=0 + while [[ ! -f "$HOME/.local/share/Steam/ubuntu12_32/steam" ]] && ((wait_count < 120)); do + sleep 5 + ((wait_count++)) + if ! kill -0 "$steam_pid" 2>/dev/null; then + # Steam process exited - check if it restarted itself (common after bootstrap) + sleep 3 + if [[ -f "$HOME/.local/share/Steam/ubuntu12_32/steam" ]]; then + break + fi + # Check if Steam relaunched under a new PID + local new_pid + new_pid=$(pgrep -f "steam" 2>/dev/null | head -1) + if [[ -n "$new_pid" ]]; then + steam_pid="$new_pid" + else + break + fi + fi + done + + if [[ -f "$HOME/.local/share/Steam/ubuntu12_32/steam" ]]; then + info "Steam client bootstrapped successfully!" + else + warn "Steam bootstrap may not have completed - you may need to run 'steam' manually" + fi + + # Give user time to log in or close Steam + echo "" + echo " You can now log into Steam or close it." + echo " The installer will continue once you press Enter." + echo "" + read -r -p "Press Enter to continue..." + + # Kill Steam if still running (we just needed the bootstrap) + pkill -f steam 2>/dev/null || true + sleep 2 + else + warn "Skipping Steam bootstrap - run 'steam' manually before using Gaming Mode" + fi + elif [[ -f "$HOME/.local/share/Steam/ubuntu12_32/steam" ]]; then + info "Steam client is fully bootstrapped" + fi + 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 +} + +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 +} + +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 +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 +} + +install_proton_ge() { + echo "" + echo "================================================================" + echo " PROTON GE (GloriousEggroll) INSTALLATION" + echo "================================================================" + echo "" + + # Detect Steam installation directory (varies by distro/package) + local steam_root="" + if [[ -d "$HOME/.local/share/Steam" ]]; then + steam_root="$HOME/.local/share/Steam" + elif [[ -L "$HOME/.steam/root" ]]; then + steam_root="$(readlink -f "$HOME/.steam/root")" + elif [[ -d "$HOME/.steam/debian-installation" ]]; then + steam_root="$HOME/.steam/debian-installation" + elif [[ -d "$HOME/.steam/steam" ]]; then + steam_root="$HOME/.steam/steam" + fi + + if [[ -z "$steam_root" || ! -d "$steam_root" ]]; then + warn "Steam directory not found" + warn "Run Steam at least once before installing Proton GE" + return 0 + fi + + info "Steam installation found at: $steam_root" + local compat_dir="$steam_root/compatibilitytools.d" + + # Check for existing Proton GE installations + local existing_ge=() + if [[ -d "$compat_dir" ]]; then + while IFS= read -r -d '' dir; do + existing_ge+=("$(basename "$dir")") + done < <(find "$compat_dir" -maxdepth 1 -type d -name "GE-Proton*" -print0 2>/dev/null | sort -zV) + fi + + if ((${#existing_ge[@]})); then + echo " Existing Proton GE installations:" + for ge in "${existing_ge[@]}"; do + echo " - $ge" + done + echo "" + else + echo " No Proton GE versions currently installed." + echo "" + fi + + # Fetch latest release info from GitHub API + info "Checking latest Proton GE release..." + local api_response + api_response=$(curl -sL --connect-timeout 10 --max-time 30 \ + "https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest" 2>/dev/null) + + if [[ -z "$api_response" ]]; then + err "Failed to fetch Proton GE release info from GitHub" + echo " Check your internet connection and try again." + return 1 + fi + + local latest_tag latest_url latest_sha_url + latest_tag=$(echo "$api_response" | grep -oP '"tag_name"\s*:\s*"\K[^"]+' | head -1) + latest_url=$(echo "$api_response" | grep -oP '"browser_download_url"\s*:\s*"\K[^"]+\.tar\.gz(?=")' | head -1) + latest_sha_url=$(echo "$api_response" | grep -oP '"browser_download_url"\s*:\s*"\K[^"]+\.sha512sum(?=")' | head -1) + + if [[ -z "$latest_tag" || -z "$latest_url" ]]; then + err "Could not parse Proton GE release info" + return 1 + fi + + echo " Latest release: $latest_tag" + echo " Download URL: $latest_url" + echo "" + + # Check if latest is already installed + if [[ -d "$compat_dir/$latest_tag" ]]; then + info "Proton GE $latest_tag is already installed!" + read -p "Reinstall $latest_tag? [y/N]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + return 0 + fi + info "Removing existing $latest_tag installation..." + rm -rf "${compat_dir:?}/$latest_tag" + else + read -p "Install Proton GE $latest_tag? [Y/n]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Nn]$ ]]; then + info "Skipping Proton GE installation" + return 0 + fi + fi + + # Create compatibility tools directory + mkdir -p "$compat_dir" + + # Download to temp directory + local tmp_dir + tmp_dir=$(mktemp -d) + local tarball="$tmp_dir/${latest_tag}.tar.gz" + + info "Downloading Proton GE $latest_tag (this may take a few minutes)..." + if ! curl -L --progress-bar --connect-timeout 15 --max-time 600 \ + -o "$tarball" "$latest_url"; then + err "Failed to download Proton GE" + rm -rf "$tmp_dir" + return 1 + fi + + # Verify checksum if available + if [[ -n "$latest_sha_url" ]]; then + info "Verifying download checksum..." + local sha_file="$tmp_dir/checksum.sha512sum" + if curl -sL --connect-timeout 10 --max-time 30 -o "$sha_file" "$latest_sha_url" 2>/dev/null; then + pushd "$tmp_dir" >/dev/null || true + # The sha512sum file references the original filename, rename our download to match + local expected_name + expected_name=$(awk '{print $2}' "$sha_file" | tr -d '*') + if [[ -n "$expected_name" && "$expected_name" != "${latest_tag}.tar.gz" ]]; then + mv "$tarball" "$tmp_dir/$expected_name" + tarball="$tmp_dir/$expected_name" + fi + if sha512sum -c "$sha_file" >/dev/null 2>&1; then + info "Checksum verified OK" + else + warn "Checksum verification failed - file may be corrupt" + read -p "Continue anyway? [y/N]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + rm -rf "$tmp_dir" + return 1 + fi + fi + popd >/dev/null || true + else + warn "Could not download checksum file - skipping verification" + fi + fi + + info "Extracting Proton GE $latest_tag..." + if ! tar -xf "$tarball" -C "$compat_dir"; then + err "Failed to extract Proton GE archive" + rm -rf "$tmp_dir" + return 1 + fi + + rm -rf "$tmp_dir" + + # Verify extraction + if [[ -d "$compat_dir/$latest_tag" ]]; then + info "Proton GE $latest_tag installed successfully to:" + info " $compat_dir/$latest_tag" + else + # Some releases extract with slightly different names - check what was created + local extracted + extracted=$(ls -td "$compat_dir"/GE-Proton* 2>/dev/null | head -1) + if [[ -n "$extracted" ]]; then + info "Proton GE installed successfully to:" + info " $extracted" + else + err "Extraction completed but Proton GE directory not found" + return 1 + fi + fi + + echo "" + echo " To use Proton GE in Steam:" + echo " 1. Restart Steam (if running)" + echo " 2. Right-click a game > Properties > Compatibility" + echo " 3. Check 'Force the use of a specific Steam Play compatibility tool'" + echo " 4. Select '$latest_tag' from the dropdown" + echo "" + + # Offer to clean up old versions + if ((${#existing_ge[@]})); then + echo " You have ${#existing_ge[@]} other Proton GE version(s) installed." + read -p "Remove old Proton GE versions? [y/N]: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + for old_ge in "${existing_ge[@]}"; do + if [[ "$old_ge" != "$latest_tag" && -d "$compat_dir/$old_ge" ]]; then + info "Removing $old_ge..." + rm -rf "${compat_dir:?}/$old_ge" + fi + done + info "Old Proton GE versions removed" + fi + fi + + return 0 +} + +install_mangoapp_from_source() { + echo "" + echo "================================================================" + echo " MANGOHUD + MANGOAPP INSTALLATION (from source)" + echo "================================================================" + echo "" + + # Check if mangoapp already exists + if command -v mangoapp >/dev/null 2>&1; then + local mango_ver + mango_ver=$(mangohud --version 2>&1 | head -1 || echo "unknown") + info "mangoapp already installed (MangoHud version: $mango_ver)" + read -p "Reinstall/update MangoHud+mangoapp from source? [y/N]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + return 0 + fi + else + info "mangoapp not found (Ubuntu/Mint mangohud package does not include it)" + info "Building MangoHud from source to get mangoapp (needed for Steam performance overlay)..." + fi + + local -a mangohud_build_deps=( + "meson" + "ninja-build" + "cmake" + "pkg-config" + "git" + "python3-mako" + "glslang-tools" + "libglew-dev" + "libglfw3-dev" + "libxnvctrl-dev" + "libdbus-1-dev" + "libx11-dev" + "libxkbcommon-dev" + "libwayland-dev" + "wayland-protocols" + "libvulkan-dev" + "libspdlog-dev" + "libfmt-dev" + "appstream" + ) + + info "Installing MangoHud build dependencies..." + if ! sudo apt install -y "${mangohud_build_deps[@]}" 2>/dev/null; then + warn "Some build dependencies failed to install - attempting recovery..." + apt_fix_and_retry "${mangohud_build_deps[@]}" || { + warn "Some build dependencies could not be installed" + warn "MangoHud build may fail - attempting anyway..." + } + fi + + local build_dir + build_dir=$(mktemp -d) + local mangohud_src="$build_dir/MangoHud" + + # Use latest release tag + info "Cloning MangoHud from GitHub..." + if ! git clone --recurse-submodules --depth 1 https://github.com/flightlessmango/MangoHud.git "$mangohud_src"; then + err "Failed to clone MangoHud repository" + rm -rf "$build_dir" + return 1 + fi + + pushd "$mangohud_src" >/dev/null || { rm -rf "$build_dir"; return 1; } + + info "Configuring MangoHud build (with mangoapp)..." + if ! meson setup build --prefix=/usr/local --buildtype=release \ + -Dwith_wayland=enabled \ + -Dwith_xnvctrl=enabled \ + -Dmangoapp=true \ + -Dmangohudctl=true; then + err "Meson configure failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + info "Building MangoHud (this may take a few minutes)..." + if ! ninja -C build; then + err "MangoHud build failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + info "Installing MangoHud..." + if ! sudo ninja -C build install; then + err "MangoHud install failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + # Update library cache so the new MangoHud libs are found + sudo ldconfig + + popd >/dev/null || true + rm -rf "$build_dir" + + # Verify mangoapp + if command -v mangoapp >/dev/null 2>&1 || [[ -x /usr/local/bin/mangoapp ]]; then + info "mangoapp installed successfully at $(command -v mangoapp 2>/dev/null || echo /usr/local/bin/mangoapp)" + else + err "mangoapp binary not found after installation" + return 1 + fi +} + +install_gamescope_from_source() { + echo "" + echo "================================================================" + echo " GAMESCOPE INSTALLATION (from source)" + echo "================================================================" + echo "" + + if command -v gamescope >/dev/null 2>&1; then + local gs_version + gs_version=$(gamescope --version 2>&1 | head -1 || echo "unknown") + info "gamescope already installed: $gs_version" + read -p "Reinstall/update gamescope from source? [y/N]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + return 0 + fi + fi + + info "gamescope is not available in Linux Mint repos - building from source..." + echo "" + + local -a build_deps=( + "meson" + "ninja-build" + "cmake" + "pkg-config" + "glslang-tools" + "git" + # gamescope core deps + "libcap-dev" + "libdrm-dev" + "libinput-dev" + "libsdl2-dev" + "libvulkan-dev" + "libwayland-dev" + "libx11-dev" + "libx11-xcb-dev" + "libxcb1-dev" + "libxcomposite-dev" + "libxcursor-dev" + "libxdamage-dev" + "libxext-dev" + "libxfixes-dev" + "libxi-dev" + "libxkbcommon-dev" + "libxmu-dev" + "libxrender-dev" + "libxres-dev" + "libxtst-dev" + "libxxf86vm-dev" + "wayland-protocols" + "hwdata" + "libpipewire-0.3-dev" + "libavif-dev" + "libdecor-0-dev" + "libdisplay-info-dev" + "libeis-dev" + "libliftoff-dev" + "libluajit-5.1-dev" + "libpixman-1-dev" + "libstb-dev" + "libsystemd-dev" + "libudev-dev" + # wlroots deps (session + XCB backends) + "libseat-dev" + "libxcb-composite0-dev" + "libxcb-dri3-dev" + "libxcb-ewmh-dev" + "libxcb-icccm4-dev" + "libxcb-image0-dev" + "libxcb-present-dev" + "libxcb-randr0-dev" + "libxcb-render0-dev" + "libxcb-render-util0-dev" + "libxcb-res0-dev" + "libxcb-shape0-dev" + "libxcb-shm0-dev" + "libxcb-util-dev" + "libxcb-xfixes0-dev" + "libxcb-xinput-dev" + ) + + info "Installing build dependencies..." + if ! sudo apt install -y "${build_deps[@]}" 2>/dev/null; then + warn "Some build dependencies failed to install - attempting recovery..." + apt_fix_and_retry "${build_deps[@]}" || { + warn "Some build dependencies could not be installed" + warn "Gamescope build may fail - attempting anyway..." + } + fi + + local build_dir + build_dir=$(mktemp -d) + local gamescope_src="$build_dir/gamescope" + + # Pin to 3.14.24: last version whose wlroots needs wayland-server >= 1.22 + # (3.14.26+ bundles wlroots that requires >= 1.23, but Mint 22.2 ships 1.22) + local gamescope_tag="3.14.24" + local wayland_ver + wayland_ver=$(pkg-config --modversion wayland-server 2>/dev/null || echo "0") + if [[ "$(printf '%s\n' "1.23" "$wayland_ver" | sort -V | head -1)" == "1.23" ]]; then + # wayland >= 1.23 available, use latest + gamescope_tag="" + info "wayland-server $wayland_ver >= 1.23 - building latest gamescope" + else + info "wayland-server $wayland_ver < 1.23 - pinning to gamescope $gamescope_tag" + fi + + info "Cloning gamescope from GitHub..." + if [[ -n "$gamescope_tag" ]]; then + if ! git clone --recursive --branch "$gamescope_tag" --depth 1 https://github.com/ValveSoftware/gamescope.git "$gamescope_src"; then + err "Failed to clone gamescope $gamescope_tag" + rm -rf "$build_dir" + return 1 + fi + else + if ! git clone --recursive https://github.com/ValveSoftware/gamescope.git "$gamescope_src"; then + err "Failed to clone gamescope repository" + rm -rf "$build_dir" + return 1 + fi + fi + + pushd "$gamescope_src" >/dev/null || { rm -rf "$build_dir"; return 1; } + + # Backport fix from gamescope 3.16.15 (issue #1934): replace YCbCr sampler + # with a regular sampler. Without this, gamescope crashes on GPUs where RADV + # doesn't support samplerYcbcrConversion (e.g. RDNA 4 / GFX1200). + # Arch/CachyOS ship gamescope 3.16.22+ which already has this fix. + # NV12 direct scanout is disabled but has zero impact on gaming. + if [[ -n "$gamescope_tag" ]] && grep -q 'CreateSamplerYcbcrConversion' src/rendervulkan.cpp 2>/dev/null; then + info "Applying RDNA 4 compatibility fix (backport from 3.16.15)..." + # Skip YCbCr conversion creation (function missing on RDNA 4 RADV) + sed -i 's|vk.CreateSamplerYcbcrConversion( device(), &ycbcrSamplerConversionCreateInfo, nullptr, &m_ycbcrConversion );|m_ycbcrConversion = VK_NULL_HANDLE; // RDNA4: skip unsupported YCbCr|' src/rendervulkan.cpp + # Create a regular sampler instead of YCbCr sampler (remove pNext) + sed -i 's|\.pNext = &ycbcrSamplerConversionInfo,|.pNext = nullptr, // RDNA4: regular sampler|' src/rendervulkan.cpp + fi + + info "Configuring gamescope build..." + if ! meson setup build --prefix=/usr/local --buildtype=release -Dpipewire=enabled; then + err "Meson configure failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + info "Building gamescope (this may take a few minutes)..." + if ! ninja -C build; then + err "Gamescope build failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + info "Installing gamescope..." + if ! sudo ninja -C build install; then + err "Gamescope install failed" + popd >/dev/null || true + rm -rf "$build_dir" + return 1 + fi + + popd >/dev/null || true + rm -rf "$build_dir" + + # Verify + if command -v gamescope >/dev/null 2>&1 || [[ -x /usr/local/bin/gamescope ]]; then + info "gamescope installed successfully at $(command -v gamescope 2>/dev/null || echo /usr/local/bin/gamescope)" + else + err "gamescope binary not found after installation" + return 1 + fi +} + +install_chimera_session_scripts() { + echo "" + echo "================================================================" + echo " CHIMERAOS SESSION SCRIPTS (from source)" + echo "================================================================" + echo "" + + local install_dir="/usr/share/gamescope-session-plus" + local needs_base=false + local needs_steam=false + + if [[ ! -d "$install_dir" ]] || [[ ! -x "$install_dir/gamescope-session-plus" ]]; then + needs_base=true + fi + + local -a required_steam_scripts=( + "/usr/bin/steamos-session-select" + "/usr/bin/steamos-update" + "/usr/bin/jupiter-biosupdate" + "/usr/bin/steamos-select-branch" + "$install_dir/sessions.d/steam" + ) + + for script in "${required_steam_scripts[@]}"; do + if [[ ! -f "$script" ]]; then + needs_steam=true + break + fi + done + + if [[ "$needs_base" == "false" && "$needs_steam" == "false" ]]; then + info "ChimeraOS session scripts already installed" + return 0 + fi + + local build_dir + build_dir=$(mktemp -d) + + if $needs_base; then + info "Installing gamescope-session-plus from ChimeraOS..." + if git clone https://github.com/ChimeraOS/gamescope-session.git "$build_dir/gamescope-session"; then + pushd "$build_dir/gamescope-session" >/dev/null || true + sudo mkdir -p "$install_dir" + # Install the main session script + if [[ -f "gamescope-session-plus" ]]; then + sudo cp gamescope-session-plus "$install_dir/gamescope-session-plus" + sudo chmod 755 "$install_dir/gamescope-session-plus" + elif [[ -f "usr/share/gamescope-session-plus/gamescope-session-plus" ]]; then + sudo cp -r usr/share/gamescope-session-plus/* "$install_dir/" + sudo chmod 755 "$install_dir/gamescope-session-plus" + else + # Try make install or find the script + find . -name "gamescope-session-plus" -type f | while read -r f; do + sudo cp "$f" "$install_dir/gamescope-session-plus" + sudo chmod 755 "$install_dir/gamescope-session-plus" + done + fi + # Install any helper files + for f in gamescope-session-plus-*.sh; do + [[ -f "$f" ]] && sudo cp "$f" "$install_dir/" && sudo chmod 755 "$install_dir/$f" + done + popd >/dev/null || true + info "gamescope-session-plus installed" + else + err "Failed to clone gamescope-session repository" + fi + fi + + if $needs_steam; then + info "Installing gamescope-session-steam scripts from ChimeraOS..." + if git clone https://github.com/ChimeraOS/gamescope-session-steam.git "$build_dir/gamescope-session-steam"; then + pushd "$build_dir/gamescope-session-steam" >/dev/null || true + + # Install the critical sessions.d/steam config (sets CLIENTCMD and Steam env vars) + if [[ -f "usr/share/gamescope-session-plus/sessions.d/steam" ]]; then + sudo mkdir -p "$install_dir/sessions.d" + sudo cp "usr/share/gamescope-session-plus/sessions.d/steam" "$install_dir/sessions.d/steam" + sudo chmod 755 "$install_dir/sessions.d/steam" + info "Installed sessions.d/steam config (CLIENTCMD + Steam environment)" + else + warn "sessions.d/steam not found in repo - creating manually" + sudo mkdir -p "$install_dir/sessions.d" + sudo tee "$install_dir/sessions.d/steam" > /dev/null << 'STEAM_SESSION_CONF' +#! /bin/bash + +function short_session_recover { + mkdir -p ~/.local/share/Steam + rm -rf --one-file-system ~/.local/share/Steam/config/widevine + steamos-session-select desktop +} + +# Show VRR controls in Steam +export STEAM_GAMESCOPE_VRR_SUPPORTED=1 + +# Enable Mangoapp +export STEAM_MANGOAPP_PRESETS_SUPPORTED=1 +export STEAM_USE_MANGOAPP=1 + +export STEAM_USE_DYNAMIC_VRS=1 + +# Support for gamescope tearing with GAMESCOPE_ALLOW_TEARING atom +export STEAM_GAMESCOPE_HAS_TEARING_SUPPORT=1 +export STEAM_GAMESCOPE_TEARING_SUPPORTED=1 +export STEAM_GAMESCOPE_HDR_SUPPORTED=1 + +# Workaround for steam getting killed immediately during reboot +export STEAMOS_STEAM_REBOOT_SENTINEL="/tmp/steamos-reboot-sentinel" +export REBOOT_SENTINEL=$STEAMOS_STEAM_REBOOT_SENTINEL + +export STEAMOS_STEAM_SHUTDOWN_SENTINEL="/tmp/steamos-shutdown-sentinel" +export SHUTDOWN_SENTINEL=$STEAMOS_STEAM_SHUTDOWN_SENTINEL + +export STEAM_ENABLE_VOLUME_HANDLER=1 +export SRT_URLOPEN_PREFER_STEAM=1 +export STEAM_DISABLE_AUDIO_DEVICE_SWITCHING=1 +export STEAM_MULTIPLE_XWAYLANDS=1 +export STEAM_GAMESCOPE_DYNAMIC_FPSLIMITER=1 +export STEAM_GAMESCOPE_NIS_SUPPORTED=1 +export STEAM_ALLOW_DRIVE_UNMOUNT=1 +export STEAM_DISABLE_MANGOAPP_ATOM_WORKAROUND=1 +export STEAM_MANGOAPP_HORIZONTAL_SUPPORTED=1 +export STEAM_GAMESCOPE_FANCY_SCALING_SUPPORT=1 +export STEAM_GAMESCOPE_COLOR_MANAGED=1 +export STEAM_GAMESCOPE_VIRTUAL_WHITE=1 + +export QT_IM_MODULE=steam +export GTK_IM_MODULE=Steam + +export CURSOR_FILE="${HOME}/.local/share/Steam/tenfoot/resource/images/cursors/arrow.png" + +export CLIENTCMD="steam -gamepadui -steamos3 -steampal -steamdeck" + +touch "${HOME}"/.steam/root/config/SteamAppData.vdf || true +STEAM_SESSION_CONF + sudo chmod 755 "$install_dir/sessions.d/steam" + info "Created sessions.d/steam config manually" + fi + + # Install Steam compatibility stubs + for script_name in steamos-session-select steamos-update jupiter-biosupdate steamos-select-branch; do + local src_file="" + # Search common locations in the repo + for candidate in "usr/bin/$script_name" "$script_name" "bin/$script_name"; do + if [[ -f "$candidate" ]]; then + src_file="$candidate" + break + fi + done + if [[ -n "$src_file" ]]; then + sudo cp "$src_file" "/usr/bin/$script_name" + sudo chmod 755 "/usr/bin/$script_name" + info "Installed /usr/bin/$script_name" + else + # Create stub if not found in repo + info "Creating stub for $script_name..." + case "$script_name" in + steamos-session-select) + sudo tee "/usr/bin/$script_name" > /dev/null << 'STUB_SELECT' +#!/bin/bash +# ChimeraOS compatibility stub for Steam +case "$1" in + desktop) + exec /usr/lib/os-session-select + ;; + gamescope) + echo "Already in gamescope session" + ;; + *) + echo "Usage: $0 {desktop|gamescope}" + ;; +esac +STUB_SELECT + ;; + steamos-update) + sudo tee "/usr/bin/$script_name" > /dev/null << 'STUB_UPDATE' +#!/bin/bash +# ChimeraOS compatibility stub - no SteamOS updates on Linux Mint +echo "System updates are managed through Update Manager" +exit 0 +STUB_UPDATE + ;; + jupiter-biosupdate) + sudo tee "/usr/bin/$script_name" > /dev/null << 'STUB_BIOS' +#!/bin/bash +# ChimeraOS compatibility stub - not applicable on desktop +exit 0 +STUB_BIOS + ;; + steamos-select-branch) + sudo tee "/usr/bin/$script_name" > /dev/null << 'STUB_BRANCH' +#!/bin/bash +# ChimeraOS compatibility stub +echo "Branch selection not available on Linux Mint" +exit 0 +STUB_BRANCH + ;; + esac + sudo chmod 755 "/usr/bin/$script_name" + fi + done + + # Install session-specific files (gamescope-session-steam script) + for f in gamescope-session-steam*.sh; do + [[ -f "$f" ]] && sudo cp "$f" "$install_dir/" && sudo chmod 755 "$install_dir/$f" + done + + popd >/dev/null || true + info "gamescope-session-steam scripts installed" + else + err "Failed to clone gamescope-session-steam repository" + fi + fi + + rm -rf "$build_dir" +} + +setup_requirements() { + local -a required_packages=("steam" "mangohud" "python3-evdev" "libcap2-bin" "gamemode" "curl" "pciutils" "ntfs-3g") + 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 + if ! sudo apt install -y "${packages_to_install[@]}"; then + warn "Initial install failed - attempting recovery..." + if ! apt_fix_and_retry "${packages_to_install[@]}"; then + local -a still_needed=() + for pkg in "${packages_to_install[@]}"; do + check_package "$pkg" || still_needed+=("$pkg") + done + if ((${#still_needed[@]})); then + err "Could not install: ${still_needed[*]}" + read -p "Continue anyway? [y/N]: " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] || die "Required packages missing - cannot continue" + fi + fi + fi + else + die "Required packages missing - cannot continue" + fi + else + info "All required packages present." + fi + + setup_performance_permissions + setup_shader_cache + + # Build/install gamescope from source + install_gamescope_from_source + + # Build/install MangoHud with mangoapp from source + # (Ubuntu/Mint mangohud package does NOT include mangoapp, + # which is required for Steam's in-gamescope performance overlay) + if ! command -v mangoapp >/dev/null 2>&1 && ! [[ -x /usr/local/bin/mangoapp ]]; then + install_mangoapp_from_source + fi + + # Install ChimeraOS session scripts from source + install_chimera_session_scripts + + # Install Proton GE for better game compatibility + install_proton_ge + + local gamescope_bin="" + if command -v gamescope >/dev/null 2>&1; then + gamescope_bin="$(command -v gamescope)" + elif [[ -x /usr/local/bin/gamescope ]]; then + gamescope_bin="/usr/local/bin/gamescope" + fi + + if [[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && [[ -n "$gamescope_bin" ]]; then + if ! getcap "$gamescope_bin" 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' "$gamescope_bin" || warn "Failed to set capability" + info "Capability granted to gamescope" + fi + fi + fi +} + +setup_session_switching() { + echo "" + echo "================================================================" + echo " SESSION SWITCHING SETUP (Cinnamon <-> Gamescope)" + echo " Using ChimeraOS gamescope-session packages" + echo "================================================================" + echo "" + + # Intel-only check + 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 " - Configure session switching between Cinnamon and Gaming Mode" + echo " - Configure Super+Shift+G to switch to Gaming Mode" + echo " - Configure Steam's 'Exit to Desktop' to return to Cinnamon" + 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" + 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 + 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 + + # Re-install ChimeraOS session scripts (they were cleaned above) + install_chimera_session_scripts + + # NetworkManager is always running on Mint - just ensure it's enabled + info "Verifying NetworkManager is enabled (default on Linux Mint)..." + if systemctl is-active --quiet NetworkManager.service; then + info "NetworkManager is running (default on Mint)" + else + warn "NetworkManager is not running - enabling..." + sudo systemctl enable --now NetworkManager.service 2>/dev/null || warn "Failed to start NetworkManager" + fi + + # NM start/stop scripts simplified for Mint (NM is always running) + local nm_start_script="/usr/local/bin/gamescope-nm-start" + sudo tee "$nm_start_script" > /dev/null << 'NM_START' +#!/bin/bash +# On Linux Mint, NetworkManager is always running. +# This script ensures NM is active and ready for the gaming session. +LOG_TAG="gamescope-nm" +log() { logger -t "$LOG_TAG" "$*"; echo "$*"; } + +if systemctl is-active --quiet NetworkManager.service; then + log "NetworkManager is running" +else + log "Starting NetworkManager service..." + systemctl start NetworkManager.service + if [ $? -eq 0 ]; then + log "NetworkManager started successfully" + else + log "ERROR: Failed to start NetworkManager" + exit 1 + fi + 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 +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 +# On Linux Mint, we do NOT stop NetworkManager - it manages all networking. +# This script is a no-op placeholder for compatibility. +LOG_TAG="gamescope-nm" +log() { logger -t "$LOG_TAG" "$*"; echo "$*"; } +log "Gaming session ended - NetworkManager remains active (Mint default)" +NM_STOP + sudo chmod +x "$nm_stop_script" + info "Created NetworkManager start/stop scripts (simplified for Mint)" + + 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 - use 'sudo' group instead of 'wheel' for Debian/Ubuntu + 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("sudo")) { + 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 + + 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("sudo")) { + 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 + + 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 + # For AMD dual-GPU systems, explicitly set VULKAN_ADAPTER to the dGPU + local amd_vulkan_adapter="" + local amd_device_id + amd_device_id=$(/usr/bin/lspci -nn | grep -iE 'amd|radeon' | grep -iE 'vga|3d|display' | grep -oP '\[1002:\K[0-9a-fA-F]+' | head -1) + [[ -n "$amd_device_id" ]] && amd_vulkan_adapter="VULKAN_ADAPTER=1002:${amd_device_id}" + + cat > "$gamescope_conf" << GAMESCOPE_CONF +SCREEN_WIDTH=${monitor_width} +SCREEN_HEIGHT=${monitor_height} +CUSTOM_REFRESH_RATES=${monitor_refresh} +${output_connector} +${amd_vulkan_adapter} +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" + + 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" + + # The gamescope binary may be at /usr/local/bin/ (built from source) or /usr/bin/ + local gamescope_real="/usr/local/bin/gamescope" + [[ -x "$gamescope_real" ]] || gamescope_real="/usr/bin/gamescope" + + sudo tee "$nvidia_wrapper" > /dev/null << NVIDIA_WRAPPER +#!/bin/bash +EXTRA_ARGS="" +if ${gamescope_real} --help 2>&1 | grep -q "force-composition"; then + EXTRA_ARGS="--force-composition" +fi +exec ${gamescope_real} \$EXTRA_ARGS "\$@" +NVIDIA_WRAPPER + + sudo chmod +x "$nvidia_wrapper" + info "Created $nvidia_wrapper" + + 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..." + + 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 + done + for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + echo performance > "$gov" 2>/dev/null + done + + if command -v nvidia-smi &>/dev/null; then + sudo -n nvidia-smi -pm 1 2>/dev/null && log "NVIDIA persistence mode enabled" + + 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 + + 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 + + # AMD GPU performance mode + for perf_file in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + if [[ -w "$perf_file" ]]; then + echo high > "$perf_file" 2>/dev/null && log "AMD GPU set to high performance" + fi + done + + 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..." + + for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + echo powersave > "$gov" 2>/dev/null + done + + if command -v nvidia-smi &>/dev/null; then + 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 + + 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 + + sudo -n nvidia-smi -pm 0 2>/dev/null + fi + + # AMD GPU restore auto performance level + for perf_file in /sys/class/drm/card*/device/power_dpm_force_performance_level; do + if [[ -w "$perf_file" ]]; then + echo auto > "$perf_file" 2>/dev/null + fi + done + + 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 + +if /usr/bin/lspci 2>/dev/null | grep -qi nvidia; then + export PATH="/usr/local/lib/gamescope-nvidia:$PATH" +fi + +# Tell gamescope-session-plus where our gamescope binary is +# (built from source to /usr/local/bin, not the default /usr/bin) +if [[ -x /usr/local/bin/gamescope ]]; then + export GAMESCOPE_BIN="/usr/local/bin/gamescope" + log "Using gamescope at $GAMESCOPE_BIN" +fi + +sudo -n /usr/local/bin/gamescope-nm-start 2>/dev/null || { + log "Warning: Could not run NM start script" +} + +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: python3-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" + + info "Creating session entry..." + local session_desktop="/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop" + sudo mkdir -p /usr/share/wayland-sessions + + 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" + + 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 + +# Graceful gamescope shutdown (releases DRM master - critical for AMD) +pkill -TERM gamescope 2>/dev/null || true +pkill -TERM -f gamescope-session 2>/dev/null || true +for _ in {1..10}; do + pgrep -x gamescope >/dev/null 2>&1 || break + sleep 0.3 +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 + sleep 1 +fi + +# Wait for DRM device to be free (amdgpu needs this before LightDM can start) +for _ in {1..10}; do + if ! fuser /dev/dri/card* 2>/dev/null | grep -q .; then + break + fi + sleep 0.3 +done + +nohup sudo -n systemctl restart lightdm &>/dev/null & +disown +exit 0 +OS_SESSION_SELECT + + sudo chmod +x "$os_session_select" + info "Created $os_session_select" + + 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 + +# Kill any stale gamescope from a previous session (graceful first for DRM cleanup) +pkill -TERM gamescope 2>/dev/null || true +pkill -TERM -f gamescope-session 2>/dev/null || true +for _ in {1..10}; do + pgrep -x gamescope >/dev/null 2>&1 || break + sleep 0.3 +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 + sleep 1 +fi + +# Find the VT that LightDM is using (fallback to 7) +lightdm_vt=$(cat /var/run/lightdm.vt 2>/dev/null || echo "") +if [[ -z "$lightdm_vt" ]]; then + lightdm_vt=$(sudo -n fgconsole 2>/dev/null || echo "7") +fi +sudo -n chvt "$lightdm_vt" 2>/dev/null || sudo -n chvt 7 2>/dev/null || true +sleep 0.5 +sudo -n systemctl restart lightdm +SWITCH_SCRIPT + + sudo chmod +x "$switch_script" + info "Created $switch_script" + + 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 + +# Graceful shutdown (lets gamescope release DRM master - critical for AMD) +pkill -TERM gamescope 2>/dev/null || true +pkill -TERM -f gamescope-session 2>/dev/null || true + +for _ in {1..10}; 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 + sleep 1 +fi + +# Wait for DRM master to be released (amdgpu needs this) +for _ in {1..10}; do + if ! fuser /dev/dri/card* 2>/dev/null | grep -q .; then + break + fi + sleep 0.3 +done + +# Find the VT that LightDM is using (fallback to 7) +lightdm_vt=$(cat /var/run/lightdm.vt 2>/dev/null || echo "") +if [[ -z "$lightdm_vt" ]]; then + lightdm_vt="7" +fi +sudo -n chvt "$lightdm_vt" 2>/dev/null || sudo -n chvt 7 2>/dev/null || true +sleep 0.5 +sudo -n systemctl stop lightdm 2>/dev/null || true +sleep 1 +sudo -n systemctl start lightdm & +disown +exit 0 +SWITCH_DESKTOP + + sudo chmod +x "$switch_desktop_script" + info "Created $switch_desktop_script" + + 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: python3-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" + + info "Creating LightDM session switching config..." + local lightdm_gaming_conf="/etc/lightdm/lightdm.conf.d/80-gaming-session.conf" + + local autologin_user="$current_user" + + sudo mkdir -p /etc/lightdm/lightdm.conf.d + + sudo tee "$lightdm_gaming_conf" > /dev/null << LIGHTDM_GAMING +[Seat:*] +autologin-user=${autologin_user} +autologin-session=cinnamon +LIGHTDM_GAMING + + info "Created $lightdm_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/lightdm/lightdm.conf.d/80-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/^autologin-session=.*/autologin-session=gamescope-session-steam-nm/' "$CONF" + echo "Session set to: gaming mode" + ;; + desktop) + sed -i 's/^autologin-session=.*/autologin-session=cinnamon/' "$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" + + 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 lightdm +%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop lightdm +%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl start lightdm +%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 +%sudo ALL=(ALL) NOPASSWD: /usr/bin/systemctl start NetworkManager.service +%sudo 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 +%sudo ALL=(ALL) NOPASSWD: /usr/local/bin/gamescope-nm-start +%sudo 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 + + # Cinnamon keybinding setup (Super+Shift+G) + info "Adding Cinnamon keybind (Super+Shift+G)..." + setup_cinnamon_keybind + + echo "" + echo "================================================================" + echo " SESSION SWITCHING CONFIGURED (ChimeraOS on Mint)" + echo "================================================================" + echo "" + echo " Usage:" + echo " - Press Super+Shift+G in Cinnamon to switch to Gaming Mode" + echo " - Press Super+Shift+R in Gaming Mode to return to Cinnamon" + echo " - (Steam's Power > Exit to Desktop also works as fallback)" + echo "" + echo " ChimeraOS scripts installed from source:" + echo " - gamescope-session-plus (base session framework)" + echo " - gamescope-session-steam scripts (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 " - /etc/lightdm/lightdm.conf.d/80-gaming-session.conf" + echo " - Cinnamon custom keybinding (Super+Shift+G)" + echo "" + echo " NetworkManager integration:" + echo " - /usr/local/bin/gamescope-nm-start (simplified for Mint)" + echo " - /usr/local/bin/gamescope-nm-stop (no-op on Mint)" + echo " - /etc/polkit-1/rules.d/50-gamescope-networkmanager.rules" + echo " - /etc/sudoers.d/gaming-session-switch" + echo "" + + return 0 +} + +setup_cinnamon_keybind() { + # Check if keybinding already exists + local existing_list + existing_list=$(dconf read /org/cinnamon/desktop/keybindings/custom-list 2>/dev/null || echo "") + + if echo "$existing_list" | grep -q "__custom_gaming0"; then + info "Gaming Mode keybind already exists in Cinnamon" + return 0 + fi + + # Build the new custom-list, appending our keybind + local new_list + if [[ -z "$existing_list" || "$existing_list" == "@as []" ]]; then + new_list="['__custom_gaming0']" + else + # Remove trailing ] and append our entry + new_list="${existing_list%]}, '__custom_gaming0']" + fi + + dconf write /org/cinnamon/desktop/keybindings/custom-list "$new_list" + dconf write /org/cinnamon/desktop/keybindings/custom-keybindings/__custom_gaming0/name "'Switch to Gaming Mode'" + dconf write /org/cinnamon/desktop/keybindings/custom-keybindings/__custom_gaming0/command "'/usr/local/bin/switch-to-gaming'" + dconf write /org/cinnamon/desktop/keybindings/custom-keybindings/__custom_gaming0/binding "['g']" + + info "Added Gaming Mode keybind (Super+Shift+G) to Cinnamon" +} + +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:Cinnamon 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 script" + ["/usr/bin/steamos-update"]="755:Steam compatibility script" + ["/usr/bin/jupiter-biosupdate"]="755:Steam compatibility script" + ["/usr/bin/steamos-select-branch"]="755:Steam compatibility script" + ["/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"]="644:LightDM session entry" + ["/usr/share/gamescope-session-plus/gamescope-session-plus"]="755:ChimeraOS session launcher" + ["/usr/share/gamescope-session-plus/sessions.d/steam"]="755:Steam session config (CLIENTCMD + env vars)" + ["/etc/lightdm/lightdm.conf.d/80-gaming-session.conf"]="644:LightDM 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/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 " GAMESCOPE BINARY:" + echo " -----------------" + if command -v gamescope >/dev/null 2>&1; then + echo " ✓ gamescope found at $(command -v gamescope)" + elif [[ -x /usr/local/bin/gamescope ]]; then + echo " ✓ gamescope found at /usr/local/bin/gamescope" + else + echo " ✗ gamescope NOT found (needs to be built from source)" + all_ok=false + fi + + echo "" + echo " MANGOAPP (Performance Overlay):" + echo " --------------------------------" + if command -v mangoapp >/dev/null 2>&1; then + echo " ✓ mangoapp found at $(command -v mangoapp)" + elif [[ -x /usr/local/bin/mangoapp ]]; then + echo " ✓ mangoapp found at /usr/local/bin/mangoapp" + else + echo " ✗ mangoapp NOT found (Steam performance overlay will NOT work)" + echo " Fix: Re-run setup to build MangoHud from source with mangoapp" + all_ok=false + fi + + echo "" + echo " PROTON GE:" + echo " ----------" + local verify_steam_root="" + if [[ -d "$HOME/.local/share/Steam" ]]; then + verify_steam_root="$HOME/.local/share/Steam" + elif [[ -L "$HOME/.steam/root" ]]; then + verify_steam_root="$(readlink -f "$HOME/.steam/root")" + elif [[ -d "$HOME/.steam/debian-installation" ]]; then + verify_steam_root="$HOME/.steam/debian-installation" + elif [[ -d "$HOME/.steam/steam" ]]; then + verify_steam_root="$HOME/.steam/steam" + fi + local compat_dir="${verify_steam_root:+$verify_steam_root/compatibilitytools.d}" + local ge_versions=() + if [[ -d "$compat_dir" ]]; then + while IFS= read -r -d '' dir; do + ge_versions+=("$(basename "$dir")") + done < <(find "$compat_dir" -maxdepth 1 -type d -name "GE-Proton*" -print0 2>/dev/null | sort -zV) + fi + if ((${#ge_versions[@]})); then + echo " ✓ Proton GE installed (${#ge_versions[@]} version(s)):" + for gev in "${ge_versions[@]}"; do + echo " - $gev" + done + else + echo " ✗ Proton GE NOT installed" + echo " Re-run setup or manually download from:" + echo " https://github.com/GloriousEggroll/proton-ge-custom/releases" + all_ok=false + fi + + echo "" + echo " CINNAMON KEYBIND:" + echo " -----------------" + local cinnamon_list + cinnamon_list=$(dconf read /org/cinnamon/desktop/keybindings/custom-list 2>/dev/null || echo "") + if echo "$cinnamon_list" | grep -q "__custom_gaming0"; then + echo " ✓ Gaming Mode keybind (Super+Shift+G) configured in Cinnamon" + else + echo " ✗ Gaming Mode keybind NOT found in Cinnamon custom keybindings" + all_ok=false + fi + + echo "" + echo " CHIMERAOS SCRIPTS:" + echo " -------------------" + if [[ -x "/usr/share/gamescope-session-plus/gamescope-session-plus" ]]; then + echo " ✓ gamescope-session-plus installed" + else + echo " ✗ gamescope-session-plus NOT installed" + all_ok=false + fi + if [[ -x "/usr/bin/steamos-session-select" ]]; then + echo " ✓ steamos-session-select installed" + else + echo " ✗ steamos-session-select 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 "python3-evdev"; then + echo " ✓ python3-evdev installed" + else + echo " ✗ python3-evdev NOT installed" + keybind_ok=false + all_ok=false + fi + + if python3 -c "import evdev" 2>/dev/null; then + echo " ✓ python3-evdev importable" + else + echo " ✗ python3-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 sudo; 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 active on Mint)" + echo " LightDM: $(systemctl is-active lightdm.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 +} + +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 " Linux Mint / Cinnamon / LightDM Edition" + 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/sudo)." + 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+G" + 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 "Linux Mint / Cinnamon / LightDM Edition" + 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 "" + echo "Converted from Arch/Hyprland/SDDM to Linux Mint/Cinnamon/LightDM." + echo "Gamescope is built from source (not available in Mint repos)." + echo "ChimeraOS session scripts are cloned from GitHub." + echo "" +} + +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