Initial commit — DeckShift v0.1.0
Steam Deck-style gaming mode for Linux + Hyprland. Lineage: Super-Shift-S-Omarchy-Deck-Mode → Omarchy Deck → DeckShift (renamed for distro-portability — distro-agnostic features on the roadmap). Currently targets Omarchy (Arch + Hyprland + SDDM + iwd). Includes: - Press Super+Shift+S to enter Gaming Mode (Gamescope + Steam Big Picture), Super+Shift+R to return to desktop - NVIDIA GSP-aware driver branch selection (legacy nvidia-580xx-utils for Maxwell/Pascal/Volta cards) - AMD support (vulkan-radeon, libvdpau) - Intel support (vulkan-intel, intel-media-driver) with generation-aware performance warning before continuing - Idempotent package installs via Omarchy's omarchy-pkg-add - Optional Xbox Bluetooth controller support (xpadneo-dkms) - Settings TUI launched from Walker (deckshift-settings) for adjusting monitor / GPU / resolution / refresh rate after install: * Buffered changes — explicit Save and exit / Cancel * hyprctl-based mode detection with monitor-capability filtering * Save-time warning if values exceed monitor capability * Esc returns to main menu without crashing - Multilib check removed (Omarchy ships with multilib enabled) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
ae01a2b32f
4 changed files with 3981 additions and 0 deletions
420
README.md
Normal file
420
README.md
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
# DeckShift
|
||||||
|
|
||||||
|
**Version 0.1.0** — Steam Deck-style gaming mode for Linux + Hyprland. Press `Super+Shift+S` to enter Gaming Mode (Steam Big Picture in Gamescope), `Super+Shift+R` to return to your desktop.
|
||||||
|
|
||||||
|
Lineage: forked from [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/nosignal/Super-Shift-S-Omarchy-Deck-Mode), briefly renamed Omarchy Deck, then renamed DeckShift as the project moves toward distro-portability.
|
||||||
|
|
||||||
|
> **Current status**: targets [Omarchy](https://omarchy.com) (Arch + Hyprland + SDDM + iwd). Works on other Arch + Hyprland setups with minor manual tweaks. Cross-distro support (Fedora / openSUSE / Cachy) is the next direction.
|
||||||
|
|
||||||
|
## What's New vs Super-Shift-S
|
||||||
|
|
||||||
|
- **Gaming Mode settings TUI** — a floating gum-based TUI launched from Walker (`Super+Space → "DeckShift Settings"`) for adjusting monitor, GPU, resolution, and refresh rate after install. No more hand-editing `gamescope-session-plus.conf`.
|
||||||
|
- **NVIDIA driver branch auto-pick** — Pascal/Maxwell/Volta cards (GTX 9xx/10xx, Quadro P/M) get the legacy `nvidia-580xx-utils` driver automatically via Omarchy's `omarchy-hw-nvidia-gsp` helper. Modern Turing+ cards stay on `nvidia-utils`.
|
||||||
|
- **Idempotent package installs** — uses Omarchy's `omarchy-pkg-add` everywhere, which double-checks pacman actually installed each package.
|
||||||
|
- **Optional Xbox Bluetooth controller support** — opt-in step installs `xpadneo-dkms` for proper button mapping and rumble with wireless Xbox pads.
|
||||||
|
- **Intel GPU support** — Intel-only systems (Iris Xe, Arc) are now supported. Older Gen8/9 (Skylake/Kaby Lake) gets a performance warning before continuing. The `NO DICE CHICAGO - INTEL DETECTED` block-out is gone.
|
||||||
|
- **Multilib check removed** — Omarchy ships with multilib enabled.
|
||||||
|
|
||||||
|
## Settings TUI
|
||||||
|
|
||||||
|
After install, launch `DeckShift Settings` from Walker (or run `deckshift-settings` directly) to change Gaming Mode display settings without editing config files:
|
||||||
|
|
||||||
|
| Option | What it sets in `gamescope-session-plus.conf` |
|
||||||
|
|--------|------------------------------------------------|
|
||||||
|
| Monitor | `OUTPUT_CONNECTOR` (auto-detected from connected DRM outputs) |
|
||||||
|
| Resolution | `SCREEN_WIDTH` / `SCREEN_HEIGHT` (offers monitor's native modes + common presets) |
|
||||||
|
| Refresh rate | `CUSTOM_REFRESH_RATES` (parsed from EDID, or common rates as fallback) |
|
||||||
|
| GPU | `VULKAN_ADAPTER` + `GBM_BACKEND` (NVIDIA) or `DRI_PRIME` (AMD) — useful on hybrid laptops |
|
||||||
|
|
||||||
|
The TUI launches as a floating window via Omarchy's `TUI.float` pattern. Selections are **buffered** — nothing is written to disk until you pick **Save and exit**. **Cancel** discards unsaved changes. Saved changes apply next time you enter Gaming Mode (`Super+Shift+S`).
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
This installer transforms your desktop into a dual-mode system:
|
||||||
|
|
||||||
|
- **Desktop Mode** - Your normal Hyprland 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 - SDDM handles session transitions, and all your network, audio, and peripherals carry over automatically.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **OS**: [Omarchy](https://omarchy.com) (Arch Linux)
|
||||||
|
- **GPU**: AMD (discrete or APU), NVIDIA (discrete), or Intel (Arc / Iris Xe)
|
||||||
|
- Intel Arc (Alchemist, Battlemage): well-supported
|
||||||
|
- Tiger Lake / Alder Lake Iris Xe: playable for indies / older AAA
|
||||||
|
- Older Gen8/9 Intel (Skylake, Kaby Lake): expect slow/glitchy — installer warns and asks before continuing
|
||||||
|
- Intel iGPU + AMD/NVIDIA dGPU configurations: dGPU is used for gaming
|
||||||
|
- **AUR Helper**: yay or paru (for ChimeraOS session packages)
|
||||||
|
|
||||||
|
> **Note**: This script is designed specifically for Omarchy and its stack (Hyprland, SDDM, iwd, UWSM, PipeWire). It is not intended for other Arch installations or distributions.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.no-signal.uk/nosignal/deckshift.git
|
||||||
|
cd deckshift
|
||||||
|
chmod +x deckshift.sh
|
||||||
|
./deckshift.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer is fully interactive and will walk you through each step.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
| Action | Keybind |
|
||||||
|
|--------|---------|
|
||||||
|
| Enter Gaming Mode | `Super + Shift + S` |
|
||||||
|
| Return to Desktop | `Super + Shift + R` |
|
||||||
|
| Exit to Desktop (fallback) | Steam > Power > Exit to Desktop |
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
```
|
||||||
|
./deckshift.sh # Full installation
|
||||||
|
./deckshift.sh --verify # Verify installation only
|
||||||
|
./deckshift.sh --version # Show version
|
||||||
|
./deckshift.sh --help # Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
|
||||||
|
The installer checks for and offers to install:
|
||||||
|
|
||||||
|
**Core Steam Dependencies**
|
||||||
|
- `steam`, `gamescope`, `mangohud`, `gamemode`
|
||||||
|
- Vulkan loaders and Mesa libraries (32-bit and 64-bit)
|
||||||
|
- Audio libraries (`lib32-alsa-plugins`, `lib32-libpulse`, `lib32-openal`)
|
||||||
|
- Networking (`networkmanager`, `lib32-libnm`)
|
||||||
|
- Fonts (`ttf-liberation`)
|
||||||
|
|
||||||
|
**GPU-Specific Drivers**
|
||||||
|
- **NVIDIA (Turing+ / GSP firmware — GTX 16xx, RTX 20-50xx, etc.)**: `nvidia-utils`, `lib32-nvidia-utils`, `nvidia-settings`, `libva-nvidia-driver`
|
||||||
|
- **NVIDIA (legacy Maxwell/Pascal/Volta — GTX 9xx/10xx, Quadro P/M)**: `nvidia-580xx-utils`, `lib32-nvidia-580xx-utils`, `nvidia-settings`, `libva-nvidia-driver`
|
||||||
|
- **AMD**: `vulkan-radeon`, `lib32-vulkan-radeon`, `libvdpau`, `lib32-libvdpau`
|
||||||
|
- **Intel**: `vulkan-intel`, `lib32-vulkan-intel`, `intel-media-driver`
|
||||||
|
|
||||||
|
The correct NVIDIA driver branch is auto-selected via Omarchy's `omarchy-hw-nvidia-gsp` / `omarchy-hw-nvidia-without-gsp` helpers — no manual override needed. Intel-only systems get a generation warning + Y/N prompt before continuing (Skylake/Kaby Lake era is slow; Tiger Lake / Arc is fine).
|
||||||
|
|
||||||
|
**AUR Packages** (via yay/paru)
|
||||||
|
- `gamescope-session-git` - ChimeraOS base session framework
|
||||||
|
- `gamescope-session-steam-git` - ChimeraOS Steam session with compatibility scripts
|
||||||
|
- `proton-ge-custom-bin` (optional)
|
||||||
|
|
||||||
|
**Other Requirements**
|
||||||
|
- `python-evdev` - For the keyboard shortcut monitor
|
||||||
|
- `ntfs-3g` - For mounting NTFS game drives
|
||||||
|
- `udisks2` - For external drive auto-mounting
|
||||||
|
- `xcb-util-cursor`, `libcap`, `curl`, `pciutils`
|
||||||
|
|
||||||
|
**Optional: Xbox Bluetooth Controllers**
|
||||||
|
- `xpadneo-dkms`, `linux-headers` - Wireless Xbox pad button mapping & rumble for Big Picture / RetroArch (wired pads work without this)
|
||||||
|
- Prompted opt-in during install; pair with `Super+Ctrl+B`
|
||||||
|
|
||||||
|
Package installs use Omarchy's `omarchy-pkg-add` (idempotent, double-checks pacman actually installed each package).
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
|
||||||
|
#### Session Scripts
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/usr/local/bin/switch-to-gaming` | Switches from Hyprland to Gaming Mode |
|
||||||
|
| `/usr/local/bin/switch-to-desktop` | Switches from Gaming Mode back to Hyprland |
|
||||||
|
| `/usr/local/bin/gamescope-session-nm-wrapper` | Main session wrapper (performance mode, NM, drive mounting) |
|
||||||
|
| `/usr/local/bin/gaming-session-switch` | Helper to toggle SDDM session config between modes |
|
||||||
|
| `/usr/local/bin/gaming-keybind-monitor` | Python daemon monitoring `Super+Shift+R` 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` | Starts NetworkManager on gaming session entry |
|
||||||
|
| `/usr/local/bin/gamescope-nm-stop` | Stops NetworkManager and restores iwd on session exit |
|
||||||
|
| `/etc/NetworkManager/conf.d/10-iwd-backend.conf` | Configures NM to use iwd backend (if iwd detected) |
|
||||||
|
| `/etc/NetworkManager/conf.d/20-unmanaged-systemd.conf` | Prevents NM/systemd-networkd conflicts (if networkd detected) |
|
||||||
|
|
||||||
|
#### 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/wayland-sessions/gamescope-session-steam-nm.desktop` | SDDM session entry for Gaming Mode |
|
||||||
|
| `/etc/sddm.conf.d/zz-gaming-session.conf` | SDDM autologin session switching config |
|
||||||
|
|
||||||
|
#### 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 optimization (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) — edit via the settings TUI |
|
||||||
|
| `~/.config/hypr/bindings.conf` | Hyprland keybind for `Super+Shift+S` (appended) |
|
||||||
|
|
||||||
|
#### Settings TUI
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/usr/local/bin/deckshift-settings` | Gum-based TUI for adjusting Gaming Mode display settings |
|
||||||
|
| `/usr/share/applications/deckshift-settings.desktop` | Walker launcher (floats via Omarchy's `TUI.float` windowrule) |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Session Switching Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Desktop Mode (Hyprland)
|
||||||
|
│
|
||||||
|
├─ Super+Shift+S pressed
|
||||||
|
│ └─ switch-to-gaming runs:
|
||||||
|
│ ├─ Masks suspend targets (prevents sleep during switch)
|
||||||
|
│ ├─ Updates SDDM config to gaming session
|
||||||
|
│ └─ Restarts SDDM → boots into Gaming Mode
|
||||||
|
│
|
||||||
|
Gaming Mode (Gamescope + Steam Big Picture)
|
||||||
|
│
|
||||||
|
├─ On session start (gamescope-session-nm-wrapper):
|
||||||
|
│ ├─ Enables performance mode (CPU governor, GPU tuning)
|
||||||
|
│ ├─ Starts NetworkManager (for Steam network access)
|
||||||
|
│ ├─ Launches steam-library-mount (external drive detection)
|
||||||
|
│ ├─ Starts gaming-keybind-monitor (Super+Shift+R listener)
|
||||||
|
│ └─ Launches gamescope-session-plus with Steam
|
||||||
|
│
|
||||||
|
├─ Super+Shift+R pressed (or Steam > Exit to Desktop)
|
||||||
|
│ └─ switch-to-desktop runs:
|
||||||
|
│ ├─ Unmasks suspend targets
|
||||||
|
│ ├─ Restores Bluetooth
|
||||||
|
│ ├─ Shuts down Steam gracefully
|
||||||
|
│ ├─ Kills gamescope
|
||||||
|
│ ├─ Updates SDDM config to Hyprland session
|
||||||
|
│ └─ Restarts SDDM → boots into Desktop Mode
|
||||||
|
│
|
||||||
|
└─ On session cleanup (trap handler):
|
||||||
|
├─ Kills steam-library-mount and keybind-monitor
|
||||||
|
├─ Stops NetworkManager, restores iwd WiFi
|
||||||
|
└─ 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. Driver branch (modern vs legacy 580xx) auto-selected via Omarchy's GSP-firmware detection — older Pascal/Maxwell cards get `nvidia-580xx-utils` automatically.
|
||||||
|
- **Intel (Arc / Iris Xe / iGPU)**: Detected via `i915` / `xe` kernel drivers. Used as primary on Intel-only systems. The gamescope conf for Intel skips `ADAPTIVE_SYNC` and `ENABLE_GAMESCOPE_HDR` by default — most Intel iGPUs don't support adaptive sync, and gamescope HDR on Intel is unreliable. Users with Intel Arc + a VRR display can enable both via the settings TUI.
|
||||||
|
- **Multi-GPU**: Correctly identifies discrete vs integrated, selects dGPU for gaming. Intel iGPU + AMD/NVIDIA dGPU systems use the dGPU.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### NetworkManager Integration
|
||||||
|
|
||||||
|
Many systems running Hyprland use `iwd` or `systemd-networkd` instead of NetworkManager. Since Steam requires NetworkManager for its network settings UI, the installer creates a managed handoff:
|
||||||
|
|
||||||
|
1. On Gaming Mode entry: NetworkManager starts, takes over networking
|
||||||
|
2. On Gaming Mode exit: NetworkManager stops, iwd/networkd resumes
|
||||||
|
|
||||||
|
This avoids conflicts and ensures both desktop and gaming sessions have network access.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## NVIDIA-Specific Notes
|
||||||
|
|
||||||
|
- **Kernel parameter**: `nvidia-drm.modeset=1` is required. The installer can configure this for Limine, GRUB, or systemd-boot.
|
||||||
|
- **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 initialized, disabled on exit.
|
||||||
|
|
||||||
|
## Bootloader Support
|
||||||
|
|
||||||
|
The installer can automatically configure `nvidia-drm.modeset=1` for:
|
||||||
|
|
||||||
|
- **Limine** - Appends to `cmdline:` in `/boot/limine.conf`
|
||||||
|
- **GRUB** - Adds to `GRUB_CMDLINE_LINUX_DEFAULT` and regenerates config
|
||||||
|
- **systemd-boot** - Provides manual instructions for `/boot/loader/entries/*.conf`
|
||||||
|
|
||||||
|
A backup is created before any bootloader modification.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
Run the built-in verification to check all files, permissions, packages, and services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deckshift.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`
|
||||||
|
|
||||||
|
**No network in Gaming Mode**
|
||||||
|
- Test NM manually: `sudo systemctl start NetworkManager && nmcli general`
|
||||||
|
- Check polkit rules: `ls -la /etc/polkit-1/rules.d/50-gamescope-*`
|
||||||
|
- Check logs: `journalctl -t gamescope-nm -n 20`
|
||||||
|
|
||||||
|
**Super+Shift+R doesn't work in Gaming Mode**
|
||||||
|
- Ensure `python-evdev` is installed: `pacman -Qi python-evdev`
|
||||||
|
- Ensure user is in `input` group: `groups | grep input`
|
||||||
|
- Check keybind monitor: `journalctl -t gaming-keybind-monitor -n 20`
|
||||||
|
- Fallback: Use Steam > Power > Exit to Desktop
|
||||||
|
|
||||||
|
**External drives not mounting**
|
||||||
|
- Ensure `udisks2` is installed: `pacman -Qi 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`
|
||||||
|
|
||||||
|
**Intel-only system, Gaming Mode is laggy**
|
||||||
|
- Older Gen8/9 Intel iGPUs (Skylake, Kaby Lake) struggle with Vulkan workloads. Lower the launch resolution via the settings TUI (`deckshift-settings`) — 720p / 1080p makes a big difference.
|
||||||
|
- If you have a discrete GPU that should take over, check 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` |
|
||||||
|
|
||||||
|
## Uninstalling
|
||||||
|
|
||||||
|
To remove Gaming Mode, delete the files listed in the [Files Created](#files-created) section. Key cleanup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove scripts
|
||||||
|
sudo rm -f /usr/local/bin/switch-to-gaming
|
||||||
|
sudo rm -f /usr/local/bin/switch-to-desktop
|
||||||
|
sudo rm -f /usr/local/bin/gamescope-session-nm-wrapper
|
||||||
|
sudo rm -f /usr/local/bin/gaming-session-switch
|
||||||
|
sudo rm -f /usr/local/bin/gaming-keybind-monitor
|
||||||
|
sudo rm -f /usr/local/bin/gamescope-nm-start
|
||||||
|
sudo rm -f /usr/local/bin/gamescope-nm-stop
|
||||||
|
sudo rm -f /usr/local/bin/steam-library-mount
|
||||||
|
sudo rm -f /usr/lib/os-session-select
|
||||||
|
sudo rm -rf /usr/local/lib/gamescope-nvidia
|
||||||
|
|
||||||
|
# Remove session entry
|
||||||
|
sudo rm -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop
|
||||||
|
|
||||||
|
# Remove permissions
|
||||||
|
sudo rm -f /etc/sudoers.d/gaming-session-switch
|
||||||
|
sudo rm -f /etc/sudoers.d/gaming-mode-sysctl
|
||||||
|
sudo rm -f /etc/polkit-1/rules.d/50-gamescope-networkmanager.rules
|
||||||
|
sudo rm -f /etc/polkit-1/rules.d/50-udisks-gaming.rules
|
||||||
|
sudo rm -f /etc/udev/rules.d/99-gaming-performance.rules
|
||||||
|
sudo rm -f /etc/security/limits.d/99-gaming-memlock.conf
|
||||||
|
|
||||||
|
# Remove configs
|
||||||
|
sudo rm -f /etc/sddm.conf.d/zz-gaming-session.conf
|
||||||
|
sudo rm -f /etc/environment.d/99-shader-cache.conf
|
||||||
|
sudo rm -f /etc/environment.d/90-nvidia-gamescope.conf
|
||||||
|
sudo rm -f /etc/pipewire/pipewire.conf.d/10-gaming-latency.conf
|
||||||
|
sudo rm -f /etc/NetworkManager/conf.d/10-iwd-backend.conf
|
||||||
|
sudo rm -f /etc/NetworkManager/conf.d/20-unmanaged-systemd.conf
|
||||||
|
rm -f ~/.config/environment.d/gamescope-session-plus.conf
|
||||||
|
|
||||||
|
# Remove keybind from Hyprland (edit manually)
|
||||||
|
# Remove the "bindd = SUPER SHIFT, S, Gaming Mode..." line from ~/.config/hypr/bindings.conf
|
||||||
|
|
||||||
|
# Optionally remove AUR packages
|
||||||
|
yay -Rns gamescope-session-git gamescope-session-steam-git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- [Omarchy](https://omarchy.com) - The Arch Linux distribution this was built for
|
||||||
|
- [ChimeraOS](https://chimeraos.org/) - gamescope-session packages
|
||||||
|
- [Valve](https://store.steampowered.com/) - Steam, Gamescope, and the Steam Deck inspiration
|
||||||
|
- [Hyprland](https://hyprland.org/) - Wayland compositor
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided as-is for the Omarchy community.
|
||||||
12
applications/deckshift-settings.desktop
Normal file
12
applications/deckshift-settings.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=DeckShift Settings
|
||||||
|
GenericName=Gaming Mode Settings
|
||||||
|
Comment=Adjust monitor, GPU, resolution, and refresh rate for DeckShift Gaming Mode
|
||||||
|
Exec=xdg-terminal-exec --app-id=TUI.float -e deckshift-settings
|
||||||
|
Icon=input-gaming
|
||||||
|
Terminal=false
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Settings;Game;
|
||||||
|
Keywords=gaming;steam;gamescope;deck;monitor;refresh;gpu;deckshift;
|
||||||
509
bin/deckshift-settings
Executable file
509
bin/deckshift-settings
Executable file
|
|
@ -0,0 +1,509 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================
|
||||||
|
# deckshift-settings — Gaming Mode settings TUI
|
||||||
|
#
|
||||||
|
# Launched from Walker (Super+Space → "DeckShift Settings"). Lets the user
|
||||||
|
# adjust which monitor, GPU, resolution, and refresh rate Gaming Mode uses,
|
||||||
|
# without editing ~/.config/environment.d/gamescope-session-plus.conf by hand.
|
||||||
|
#
|
||||||
|
# Selections are buffered in memory — nothing is written until the user picks
|
||||||
|
# "Save and exit". "Cancel" discards changes. The on-disk file is rewritten in
|
||||||
|
# place, preserving any keys the TUI doesn't manage (ADAPTIVE_SYNC, HDR, etc).
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# -u (nounset) trips on empty associative arrays even with `${arr[k]+x}`
|
||||||
|
# guards, so we run with -eo pipefail only.
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
CONF="${HOME}/.config/environment.d/gamescope-session-plus.conf"
|
||||||
|
|
||||||
|
# Theme — match the "unsaved changes" amber across all gum prompts so the
|
||||||
|
# cursor / highlighted line is readable regardless of terminal palette.
|
||||||
|
ACCENT=214 # amber, also used for unsaved-changes label
|
||||||
|
HEADER_COLOR=212
|
||||||
|
export GUM_CHOOSE_CURSOR_FOREGROUND="$ACCENT"
|
||||||
|
export GUM_CHOOSE_SELECTED_FOREGROUND="$ACCENT"
|
||||||
|
export GUM_CHOOSE_HEADER_FOREGROUND="$HEADER_COLOR"
|
||||||
|
export GUM_CONFIRM_SELECTED_FOREGROUND="$ACCENT"
|
||||||
|
export GUM_CONFIRM_PROMPT_FOREGROUND="$HEADER_COLOR"
|
||||||
|
export GUM_INPUT_PROMPT_FOREGROUND="$ACCENT"
|
||||||
|
|
||||||
|
# Buffered pending changes — flushed only on Save and exit.
|
||||||
|
# PENDING_SET[KEY]=VALUE → write KEY=VALUE
|
||||||
|
# PENDING_UNSET[KEY]=1 → delete KEY from the conf
|
||||||
|
declare -A PENDING_SET
|
||||||
|
declare -A PENDING_UNSET
|
||||||
|
|
||||||
|
die() { echo "[!] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v gum >/dev/null || die "gum is required (install with: omarchy-pkg-add gum)"
|
||||||
|
|
||||||
|
# Wrappers — gum choose / gum input exit non-zero on Escape/Ctrl-C, which
|
||||||
|
# would trip `set -e` and kill the TUI. These swallow that so a cancelled
|
||||||
|
# prompt simply returns an empty string, letting the caller treat it as
|
||||||
|
# "user backed out" and return to the main menu.
|
||||||
|
gchoose() { gum choose "$@" || true; }
|
||||||
|
ginput() { gum input "$@" || true; }
|
||||||
|
command -v lspci >/dev/null || die "lspci is required"
|
||||||
|
command -v jq >/dev/null || die "jq is required (install with: omarchy-pkg-add jq)"
|
||||||
|
command -v hyprctl >/dev/null || die "hyprctl is required (this TUI is for Hyprland sessions)"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Conf helpers — read straight from disk; writes go through pending_* below so
|
||||||
|
# nothing hits the file until the user explicitly saves.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
conf_get() {
|
||||||
|
local key="$1"
|
||||||
|
[[ -f "$CONF" ]] || return 0
|
||||||
|
# awk (not grep|tail|cut) so a missing key yields empty + exit 0 rather than
|
||||||
|
# tripping pipefail and killing the TUI under `set -e`.
|
||||||
|
awk -F= -v k="$key" '$1==k { sub(/^[^=]*=/,""); v=$0 } END { print v }' "$CONF"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Effective value = pending change if any, else what's on disk. Used by the
|
||||||
|
# UI to show what the conf will look like after Save.
|
||||||
|
effective() {
|
||||||
|
local key="$1"
|
||||||
|
if [[ -n "${PENDING_UNSET[$key]:-}" ]]; then
|
||||||
|
echo ""
|
||||||
|
elif [[ -n "${PENDING_SET[$key]+x}" ]]; then
|
||||||
|
echo "${PENDING_SET[$key]}"
|
||||||
|
else
|
||||||
|
conf_get "$key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_set() {
|
||||||
|
local key="$1" value="$2"
|
||||||
|
PENDING_SET["$key"]="$value"
|
||||||
|
unset 'PENDING_UNSET[$key]'
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_unset() {
|
||||||
|
local key="$1"
|
||||||
|
PENDING_UNSET["$key"]=1
|
||||||
|
unset 'PENDING_SET[$key]'
|
||||||
|
}
|
||||||
|
|
||||||
|
has_pending_changes() {
|
||||||
|
(( ${#PENDING_SET[@]} > 0 )) || (( ${#PENDING_UNSET[@]} > 0 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write all buffered changes to disk in one pass. Preserves the order/contents
|
||||||
|
# of keys we don't manage.
|
||||||
|
flush_pending() {
|
||||||
|
has_pending_changes || return 0
|
||||||
|
mkdir -p "$(dirname "$CONF")"
|
||||||
|
touch "$CONF"
|
||||||
|
|
||||||
|
for key in "${!PENDING_UNSET[@]}"; do
|
||||||
|
sed -i "/^${key}=/d" "$CONF"
|
||||||
|
done
|
||||||
|
|
||||||
|
for key in "${!PENDING_SET[@]}"; do
|
||||||
|
local value="${PENDING_SET[$key]}"
|
||||||
|
if grep -qE "^${key}=" "$CONF"; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${value}|" "$CONF"
|
||||||
|
else
|
||||||
|
echo "${key}=${value}" >> "$CONF"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Hardware detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Cached hyprctl data — populated by refresh_monitor_data, used by all menus.
|
||||||
|
# HYPR_MODES = newline-separated WIDTHxHEIGHT@RATEHz strings for the active
|
||||||
|
# connector. HYPR_NATIVE / HYPR_MAX_REFRESH = monitor's max capability.
|
||||||
|
HYPR_MODES=""
|
||||||
|
HYPR_NATIVE=""
|
||||||
|
HYPR_MAX_REFRESH=""
|
||||||
|
HYPR_ACTIVE_CONNECTOR=""
|
||||||
|
|
||||||
|
# List every connected monitor as "NAME|WIDTHxHEIGHT". hyprctl monitors all
|
||||||
|
# returns every connected output (enabled or not), which is what we want.
|
||||||
|
list_connected_monitors() {
|
||||||
|
hyprctl monitors all -j 2>/dev/null \
|
||||||
|
| jq -r '.[] | select(.disabled==false or .disabled==true) | "\(.name)|\((.availableModes // [])[0] // "" | sub("@.*"; ""))"'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Refresh the cached mode data for the connector we're targeting. Falls back
|
||||||
|
# to the first connected output if no OUTPUT_CONNECTOR is set in the buffer/conf.
|
||||||
|
refresh_monitor_data() {
|
||||||
|
local target
|
||||||
|
target=$(effective OUTPUT_CONNECTOR)
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
target=$(hyprctl monitors -j 2>/dev/null | jq -r '.[0].name // empty')
|
||||||
|
fi
|
||||||
|
HYPR_ACTIVE_CONNECTOR="$target"
|
||||||
|
if [[ -z "$target" ]]; then
|
||||||
|
HYPR_MODES=""; HYPR_NATIVE=""; HYPR_MAX_REFRESH=""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
HYPR_MODES=$(hyprctl monitors all -j 2>/dev/null \
|
||||||
|
| jq -r --arg n "$target" '.[] | select(.name==$n) | .availableModes[]?')
|
||||||
|
HYPR_NATIVE=$(echo "$HYPR_MODES" | head -1 | sed 's/@.*//')
|
||||||
|
# Max refresh AT NATIVE resolution, rounded to integer Hz for display.
|
||||||
|
# (Reporting the max across all modes would mislead — e.g. a 4K@60 panel
|
||||||
|
# might show 100Hz at 1024x768, which isn't useful info for Gaming Mode.)
|
||||||
|
HYPR_MAX_REFRESH=$(echo "$HYPR_MODES" \
|
||||||
|
| grep "^${HYPR_NATIVE}@" \
|
||||||
|
| awk -F'@' '{ sub(/Hz$/,"",$2); printf "%d\n", $2 + 0.5 }' \
|
||||||
|
| sort -un | tail -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unique list of resolutions supported by the active connector, ordered
|
||||||
|
# largest first.
|
||||||
|
list_supported_resolutions() {
|
||||||
|
echo "$HYPR_MODES" | sed 's/@.*//' | awk '!seen[$0]++'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Refresh rates supported at a given resolution (or all rates if no res given).
|
||||||
|
# Output is integer Hz, deduplicated (60.00 and 59.94 both round to 60).
|
||||||
|
list_supported_refresh_rates() {
|
||||||
|
local res="${1:-}"
|
||||||
|
if [[ -n "$res" ]]; then
|
||||||
|
echo "$HYPR_MODES" | grep -E "^${res}@"
|
||||||
|
else
|
||||||
|
echo "$HYPR_MODES"
|
||||||
|
fi | awk -F'@' '{ sub(/Hz$/,"",$2); printf "%d\n", $2 + 0.5 }' \
|
||||||
|
| sort -un
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validation — does the requested mode fit the monitor's reported limits?
|
||||||
|
resolution_supported() {
|
||||||
|
local w="$1" h="$2"
|
||||||
|
[[ -z "$HYPR_NATIVE" ]] && return 0 # unknown → don't block
|
||||||
|
local nw=${HYPR_NATIVE%%x*}
|
||||||
|
local nh=${HYPR_NATIVE##*x}
|
||||||
|
(( w <= nw )) && (( h <= nh ))
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_rate_supported() {
|
||||||
|
local rate="$1"
|
||||||
|
[[ -z "$HYPR_MODES" ]] && return 0
|
||||||
|
echo "$HYPR_MODES" \
|
||||||
|
| awk -F'@' -v r="$rate" '{ sub(/Hz$/,"",$2); if (int($2+0.5) == int(r+0.5)) found=1 } END { exit !found }'
|
||||||
|
}
|
||||||
|
|
||||||
|
list_gpus() {
|
||||||
|
local pci_slot vendor_dev kind label
|
||||||
|
/usr/bin/lspci -nn | grep -iE 'vga|3d|display' | while read -r line; do
|
||||||
|
pci_slot=$(awk '{print $1}' <<<"$line")
|
||||||
|
vendor_dev=$(grep -oP '\[\K[0-9a-fA-F]{4}:[0-9a-fA-F]{4}(?=\])' <<<"$line" | tail -1)
|
||||||
|
if grep -qi nvidia <<<"$line"; then
|
||||||
|
kind="nvidia"
|
||||||
|
elif grep -qiE 'amd|advanced micro|radeon' <<<"$line"; then
|
||||||
|
kind="amd"
|
||||||
|
elif grep -qi intel <<<"$line"; then
|
||||||
|
kind="intel"
|
||||||
|
else
|
||||||
|
kind="other"
|
||||||
|
fi
|
||||||
|
label=$(sed -E 's/.*: //; s/ \[[0-9a-f:]+\] \(rev [0-9a-f]+\)$//; s/ \[[0-9a-f:]+\]$//' <<<"$line")
|
||||||
|
echo "${pci_slot}|${vendor_dev}|${kind}|${label}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Display — show current/effective values, marking pending edits.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
show_state() {
|
||||||
|
local connector width height refresh vk_adapter dri_prime
|
||||||
|
connector=$(effective OUTPUT_CONNECTOR)
|
||||||
|
width=$(effective SCREEN_WIDTH)
|
||||||
|
height=$(effective SCREEN_HEIGHT)
|
||||||
|
refresh=$(effective CUSTOM_REFRESH_RATES)
|
||||||
|
vk_adapter=$(effective VULKAN_ADAPTER)
|
||||||
|
dri_prime=$(effective DRI_PRIME)
|
||||||
|
|
||||||
|
local pending_label=""
|
||||||
|
has_pending_changes && pending_label=" $(gum style --foreground 214 '(unsaved changes)')"
|
||||||
|
|
||||||
|
local monitor_label="${connector:-<auto>}"
|
||||||
|
if [[ -n "$HYPR_ACTIVE_CONNECTOR" && -n "$HYPR_NATIVE" ]]; then
|
||||||
|
monitor_label="${monitor_label} (max ${HYPR_NATIVE} @ ${HYPR_MAX_REFRESH:-?}Hz)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
Gaming Mode display settings${pending_label}:
|
||||||
|
|
||||||
|
Monitor : ${monitor_label}
|
||||||
|
Resolution : ${width:-?}x${height:-?}
|
||||||
|
Refresh rate : ${refresh:-<auto>} Hz
|
||||||
|
GPU (NVIDIA) : ${vk_adapter:-<not set>}
|
||||||
|
GPU (AMD) : ${dri_prime:-<not set>}
|
||||||
|
|
||||||
|
Config file : ${CONF}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Setters — each one updates the in-memory pending buffer only.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
choose_monitor() {
|
||||||
|
local choice connector
|
||||||
|
mapfile -t connected < <(list_connected_monitors)
|
||||||
|
local -a labels=()
|
||||||
|
for entry in "${connected[@]}"; do
|
||||||
|
labels+=("$entry")
|
||||||
|
done
|
||||||
|
labels+=("(clear / let gamescope auto-pick)")
|
||||||
|
if (( ${#connected[@]} == 0 )); then
|
||||||
|
gum confirm "No connected monitors detected. Set OUTPUT_CONNECTOR manually?" || return 0
|
||||||
|
connector=$(ginput --prompt "Connector (e.g. DP-1, HDMI-A-1): ")
|
||||||
|
[[ -z "$connector" ]] && return 0
|
||||||
|
pending_set OUTPUT_CONNECTOR "$connector"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select monitor for Gaming Mode")
|
||||||
|
[[ -z "$choice" ]] && return 0
|
||||||
|
if [[ "$choice" == "(clear"* ]]; then
|
||||||
|
pending_unset OUTPUT_CONNECTOR
|
||||||
|
else
|
||||||
|
pending_set OUTPUT_CONNECTOR "${choice%%|*}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_resolution() {
|
||||||
|
refresh_monitor_data
|
||||||
|
local choice w h
|
||||||
|
local -a options=()
|
||||||
|
|
||||||
|
# 1. Monitor-supported resolutions (top of the list, always safe)
|
||||||
|
if [[ -n "$HYPR_MODES" ]]; then
|
||||||
|
while read -r res; do
|
||||||
|
[[ -z "$res" ]] && continue
|
||||||
|
options+=("$res (supported by ${HYPR_ACTIVE_CONNECTOR})")
|
||||||
|
done < <(list_supported_resolutions)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Common presets — mark anything that exceeds the detected max
|
||||||
|
local -a presets=("1280x720" "1920x1080" "2560x1440" "3840x2160")
|
||||||
|
local -A already_listed=()
|
||||||
|
while read -r r; do already_listed["$r"]=1; done < <(list_supported_resolutions)
|
||||||
|
for preset in "${presets[@]}"; do
|
||||||
|
[[ -n "${already_listed[$preset]:-}" ]] && continue
|
||||||
|
local pw=${preset%%x*} ph=${preset##*x}
|
||||||
|
if resolution_supported "$pw" "$ph"; then
|
||||||
|
options+=("$preset (preset)")
|
||||||
|
else
|
||||||
|
options+=("$preset (preset — [unsupported by monitor])")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
options+=("Custom…")
|
||||||
|
|
||||||
|
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select launch resolution (max: ${HYPR_NATIVE:-unknown})")
|
||||||
|
[[ -z "$choice" ]] && return 0
|
||||||
|
if [[ "$choice" == "Custom…" ]]; then
|
||||||
|
w=$(ginput --prompt "Width: " --placeholder "2560")
|
||||||
|
h=$(ginput --prompt "Height: " --placeholder "1440")
|
||||||
|
else
|
||||||
|
local mode=${choice%% *}
|
||||||
|
w=${mode%%x*}
|
||||||
|
h=${mode##*x}
|
||||||
|
fi
|
||||||
|
[[ -z "$w" || -z "$h" ]] && return 0
|
||||||
|
pending_set SCREEN_WIDTH "$w"
|
||||||
|
pending_set SCREEN_HEIGHT "$h"
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_refresh_rate() {
|
||||||
|
refresh_monitor_data
|
||||||
|
local choice rate
|
||||||
|
|
||||||
|
# Use the buffered/saved resolution to filter rates if set, otherwise
|
||||||
|
# show all rates this monitor supports across any resolution.
|
||||||
|
local current_w current_h res_filter=""
|
||||||
|
current_w=$(effective SCREEN_WIDTH)
|
||||||
|
current_h=$(effective SCREEN_HEIGHT)
|
||||||
|
if [[ -n "$current_w" && -n "$current_h" ]]; then
|
||||||
|
res_filter="${current_w}x${current_h}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a options=()
|
||||||
|
local -A supported_set=()
|
||||||
|
|
||||||
|
# 1. Rates supported at the active resolution (top of list)
|
||||||
|
while read -r r; do
|
||||||
|
[[ -z "$r" ]] && continue
|
||||||
|
supported_set["$r"]=1
|
||||||
|
if [[ -n "$res_filter" ]]; then
|
||||||
|
options+=("$r (supported at ${res_filter})")
|
||||||
|
else
|
||||||
|
options+=("$r (supported by ${HYPR_ACTIVE_CONNECTOR})")
|
||||||
|
fi
|
||||||
|
done < <(list_supported_refresh_rates "$res_filter")
|
||||||
|
|
||||||
|
# 2. Common presets — mark unsupported
|
||||||
|
local -a presets=(60 75 100 120 144 165 240)
|
||||||
|
for preset in "${presets[@]}"; do
|
||||||
|
[[ -n "${supported_set[$preset]:-}" ]] && continue
|
||||||
|
if refresh_rate_supported "$preset"; then
|
||||||
|
options+=("$preset (preset)")
|
||||||
|
else
|
||||||
|
options+=("$preset (preset — [unsupported by monitor])")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
options+=("Custom…")
|
||||||
|
|
||||||
|
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select refresh rate (Hz, max: ${HYPR_MAX_REFRESH:-unknown})")
|
||||||
|
[[ -z "$choice" ]] && return 0
|
||||||
|
if [[ "$choice" == "Custom…" ]]; then
|
||||||
|
rate=$(ginput --prompt "Rate (Hz): " --placeholder "144")
|
||||||
|
else
|
||||||
|
rate=${choice%% *}
|
||||||
|
fi
|
||||||
|
[[ -z "$rate" ]] && return 0
|
||||||
|
pending_set CUSTOM_REFRESH_RATES "$rate"
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_gpu() {
|
||||||
|
local choice
|
||||||
|
mapfile -t gpus < <(list_gpus)
|
||||||
|
if (( ${#gpus[@]} == 0 )); then
|
||||||
|
gum style --foreground 196 "No GPUs detected via lspci"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local -a labels=()
|
||||||
|
for entry in "${gpus[@]}"; do
|
||||||
|
IFS='|' read -r slot vendor_dev kind label <<<"$entry"
|
||||||
|
labels+=("[$kind] $label ($vendor_dev @ $slot)")
|
||||||
|
done
|
||||||
|
labels+=("(clear GPU override — let system decide)")
|
||||||
|
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select GPU for Gaming Mode")
|
||||||
|
[[ -z "$choice" ]] && return 0
|
||||||
|
if [[ "$choice" == "(clear"* ]]; then
|
||||||
|
pending_unset VULKAN_ADAPTER
|
||||||
|
pending_unset DRI_PRIME
|
||||||
|
pending_unset GBM_BACKEND
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local idx=0 selected=""
|
||||||
|
for label in "${labels[@]}"; do
|
||||||
|
if [[ "$label" == "$choice" ]]; then
|
||||||
|
selected="${gpus[$idx]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
IFS='|' read -r slot vendor_dev kind label <<<"$selected"
|
||||||
|
case "$kind" in
|
||||||
|
nvidia)
|
||||||
|
pending_set VULKAN_ADAPTER "$vendor_dev"
|
||||||
|
pending_set GBM_BACKEND "nvidia-drm"
|
||||||
|
pending_unset DRI_PRIME
|
||||||
|
;;
|
||||||
|
amd|intel|other)
|
||||||
|
local dri_tag="pci-$(echo "$slot" | sed 's/[:.]/_/g')"
|
||||||
|
[[ "$dri_tag" != pci-0000* ]] && dri_tag="pci-0000_${dri_tag#pci-}"
|
||||||
|
pending_set DRI_PRIME "$dri_tag"
|
||||||
|
pending_unset VULKAN_ADAPTER
|
||||||
|
pending_unset GBM_BACKEND
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pre-save validation. Compares the about-to-be-saved values against the
|
||||||
|
# active monitor's reported capabilities (via hyprctl) and asks for explicit
|
||||||
|
# confirmation if anything looks risky. Returns 0 = ok to save, 1 = cancelled.
|
||||||
|
confirm_risky_save() {
|
||||||
|
refresh_monitor_data
|
||||||
|
[[ -z "$HYPR_MODES" ]] && return 0
|
||||||
|
|
||||||
|
local w h rate
|
||||||
|
w=$(effective SCREEN_WIDTH)
|
||||||
|
h=$(effective SCREEN_HEIGHT)
|
||||||
|
rate=$(effective CUSTOM_REFRESH_RATES)
|
||||||
|
|
||||||
|
local -a warnings=()
|
||||||
|
if [[ -n "$w" && -n "$h" ]] && ! resolution_supported "$w" "$h"; then
|
||||||
|
warnings+=("• Resolution ${w}x${h} exceeds detected monitor max ${HYPR_NATIVE}")
|
||||||
|
fi
|
||||||
|
if [[ -n "$rate" ]] && ! refresh_rate_supported "$rate"; then
|
||||||
|
warnings+=("• Refresh rate ${rate}Hz is not in the monitor's supported list (max: ${HYPR_MAX_REFRESH:-?}Hz)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
(( ${#warnings[@]} == 0 )) && return 0
|
||||||
|
|
||||||
|
clear
|
||||||
|
gum style --foreground 196 --bold "Warning — selected values may not work:"
|
||||||
|
echo ""
|
||||||
|
printf ' %s\n' "${warnings[@]}"
|
||||||
|
echo ""
|
||||||
|
gum style --foreground 244 "If Gaming Mode shows a black screen, press Super+Shift+R to return to desktop."
|
||||||
|
echo ""
|
||||||
|
gum confirm "Save anyway?" --default=false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Main loop — buffer until Save and exit / Cancel.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
while true; do
|
||||||
|
refresh_monitor_data
|
||||||
|
clear
|
||||||
|
gum style \
|
||||||
|
--border double --margin "1" --padding "1 4" --border-foreground 212 \
|
||||||
|
"DECKSHIFT — Gaming Mode Settings"
|
||||||
|
show_state
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local save_label="Save and exit"
|
||||||
|
local cancel_label="Cancel (exit)"
|
||||||
|
if has_pending_changes; then
|
||||||
|
save_label="Save and exit ★"
|
||||||
|
cancel_label="Discard changes and exit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local action
|
||||||
|
action=$(gchoose --header "What do you want to change?" \
|
||||||
|
"Monitor" \
|
||||||
|
"Resolution" \
|
||||||
|
"Refresh rate" \
|
||||||
|
"GPU" \
|
||||||
|
"$save_label" \
|
||||||
|
"$cancel_label")
|
||||||
|
case "$action" in
|
||||||
|
"Monitor") choose_monitor ;;
|
||||||
|
"Resolution") choose_resolution ;;
|
||||||
|
"Refresh rate") choose_refresh_rate ;;
|
||||||
|
"GPU") choose_gpu ;;
|
||||||
|
"Save and exit"*)
|
||||||
|
if ! confirm_risky_save; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
flush_pending
|
||||||
|
clear
|
||||||
|
gum style --foreground 212 "Settings saved to $CONF"
|
||||||
|
gum style --foreground 244 "Changes apply next time you enter Gaming Mode (Super+Shift+S)."
|
||||||
|
sleep 1
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
"Cancel (exit)"|"Discard changes and exit")
|
||||||
|
if has_pending_changes; then
|
||||||
|
gum confirm "Discard unsaved changes?" --default=true || continue
|
||||||
|
fi
|
||||||
|
clear
|
||||||
|
gum style --foreground 244 "No changes saved."
|
||||||
|
sleep 1
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
# Esc / Ctrl-C on the main menu — stay on main rather than exit.
|
||||||
|
# User must pick "Cancel" or "Save and exit" explicitly to leave.
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
3040
deckshift.sh
Executable file
3040
deckshift.sh
Executable file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue