Author SHA1 Message Date
admin 738014ae54 updated docs 2026-07-11 17:49:19 -04:00
admin 4b7725849f added dms greeter auto session switch 2026-07-11 17:48:08 -04:00
3 changed files with 761 additions and 71 deletions
+25 -13
View File
@@ -4,12 +4,18 @@
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.
> **Target:** [Omarchy](https://omarchy.com) — Arch + Hyprland + SDDM + Walker. DeckShift depends on Omarchy-specific helpers (`omarchy-pkg-add`, `omarchy-restart-walker`, etc.) and is not intended to be cross-distro.
> **Target:** [Omarchy](https://omarchy.com) — Arch + Hyprland + SDDM + Walker. Experimental DMS/greetd manual session switching is available through `DECKSHIFT_SESSION_BACKEND=dms-greeter-manual`. DeckShift still depends on Omarchy-specific helpers (`omarchy-pkg-add`, `omarchy-restart-walker`, etc.) and is not intended to be cross-distro.
[![DeckShift demo](https://img.youtube.com/vi/nj4pLh3spCs/maxresdefault.jpg)](https://youtu.be/nj4pLh3spCs)
## What's New
### Unreleased — Session backend abstraction
- DeckShift now writes `/etc/deckshift/session-backend.conf` and routes mode selection through `/usr/local/bin/deckshift-session-switch` plus `/usr/local/bin/deckshift-session-transition`.
- `sddm` preserves the existing behavior: update `/etc/sddm.conf.d/zz-gaming-session.conf`, then restart SDDM.
- `dms-greeter-manual` is experimental: it writes DMS greeter memory to select the target session, then terminates the current login session so the user authenticates through DMS. It does not implement one-shot auto-login yet.
### v0.1.13 — Pacman hook keeps gamescope's cap_sys_nice across upgrades
- Linux file capabilities live as an xattr (`security.capability`) on the inode, so every time pacman replaces `/usr/bin/gamescope` during an upgrade the previously-granted `cap_sys_nice=eip` is silently lost. Performance mode keeps "working" but the compositor thread loses its priority boost — worse frame pacing and input latency, with no error surfaced anywhere.
@@ -117,11 +123,11 @@ 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 your network, audio, and peripherals carry over automatically.
Switching between modes is handled by a session backend. SDDM remains the default and preserves the seamless auto-login flow. The experimental DMS greeter backend preselects the next session and returns to the greeter for manual authentication.
## Requirements
- **OS**: [Omarchy](https://omarchy.com) (Arch Linux + Hyprland + SDDM)
- **OS**: [Omarchy](https://omarchy.com) (Arch Linux + Hyprland + SDDM), or an experimental DMS/greetd setup for manual session switching
- **GPU**: AMD (discrete or APU), NVIDIA (discrete), or Intel (Arc / Iris Xe), or any hybrid combo of the above
- Intel Arc (Alchemist, Battlemage): well-supported
- Tiger Lake / Alder Lake Iris Xe: playable for indies / older AAA
@@ -129,7 +135,7 @@ Switching between modes is seamless — SDDM handles session transitions, and yo
- Hybrid laptops (NVIDIA + iGPU, AMD dGPU + iGPU): use the corresponding `[hybrid-*]` GPU mode in the Settings TUI
- **AUR Helper**: yay or paru (for ChimeraOS session packages)
> **Note**: This script targets Omarchy and its stack (Hyprland, SDDM, iwd, UWSM, PipeWire). It works on other Arch + Hyprland setups with light tweaks, but isn't tested there.
> **Note**: This script targets Omarchy and its stack (Hyprland, SDDM, iwd, UWSM, PipeWire). The DMS/greetd backend is experimental and currently manual-only.
## Quick Start
@@ -234,9 +240,11 @@ Package installs use Omarchy's `omarchy-pkg-add` (idempotent, double-checks pacm
| Path | Purpose |
|---|---|
| `/usr/local/bin/switch-to-gaming` | Hyprland → Gaming Mode |
| `/usr/local/bin/switch-to-desktop` | Gaming Mode → Hyprland (synchronous power-state restore + atomic SDDM restart) |
| `/usr/local/bin/switch-to-desktop` | Gaming Mode → Hyprland (synchronous power-state restore + backend transition) |
| `/usr/local/bin/gamescope-session-nm-wrapper` | Main session wrapper (performance mode, NM, drive mounting, saves pre-Gaming-Mode state) |
| `/usr/local/bin/gaming-session-switch` | Helper that toggles SDDM autologin between Hyprland and Gamescope |
| `/usr/local/bin/deckshift-session-switch` | Backend-neutral helper that selects desktop or gaming session |
| `/usr/local/bin/deckshift-session-transition` | Backend-neutral helper that restarts SDDM or exits to DMS greeter |
| `/usr/local/bin/gaming-session-switch` | Compatibility wrapper for `deckshift-session-switch` |
| `/usr/local/bin/gaming-keybind-monitor` | Python evdev daemon catching `Super+Shift+R` inside Gamescope |
| `/usr/lib/os-session-select` | Handler for Steam's "Exit to Desktop" button |
| `/usr/local/lib/gamescope-nvidia/gamescope` | NVIDIA wrapper that adds `--force-composition` |
@@ -257,8 +265,10 @@ Package installs use Omarchy's `omarchy-pkg-add` (idempotent, double-checks pacm
#### 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 |
| `/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop` | Wayland session entry for Gaming Mode |
| `/etc/deckshift/session-backend.conf` | Active session backend and session IDs |
| `/etc/sddm.conf.d/zz-gaming-session.conf` | SDDM autologin session switching config when using the SDDM backend |
| `/var/cache/dms-greeter/.local/state/memory.json` | DMS greeter memory updated when using the DMS manual backend |
#### Permissions & Security
| Path | Purpose |
@@ -300,8 +310,8 @@ 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
│ ├─ Selects the gaming session through deckshift-session-switch
│ └─ Runs deckshift-session-transition
Gaming Mode (Gamescope + Steam Big Picture)
@@ -320,8 +330,8 @@ Gaming Mode (Gamescope + Steam Big Picture)
│ ├─ Restores Bluetooth
│ ├─ Shuts down Steam gracefully
│ ├─ Kills gamescope
│ ├─ Updates SDDM config to Hyprland session
│ └─ Atomic systemctl restart sddm → boots into Desktop Mode
│ ├─ Selects the desktop session through deckshift-session-switch
│ └─ Runs deckshift-session-transition
└─ On session cleanup (trap handler — backup path):
├─ Kills steam-library-mount and keybind-monitor
@@ -553,7 +563,8 @@ sudo pkill -f steam-library-mount
# Remove scripts
sudo rm -f /usr/local/bin/{switch-to-gaming,switch-to-desktop,gamescope-session-nm-wrapper,\
gaming-session-switch,gaming-keybind-monitor,gamescope-nm-start,gamescope-nm-stop,\
deckshift-session-switch,deckshift-session-transition,gaming-session-switch,\
gaming-keybind-monitor,gamescope-nm-start,gamescope-nm-stop,\
steam-library-mount,deckshift-settings}
sudo rm -f /usr/lib/os-session-select
sudo rm -rf /usr/local/lib/gamescope-nvidia
@@ -572,6 +583,7 @@ 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 -rf /etc/deckshift
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
+311 -56
View File
@@ -48,6 +48,9 @@ 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}"
: "${DECKSHIFT_SESSION_BACKEND:=auto}"
: "${DECKSHIFT_DESKTOP_SESSION_ID:=hyprland-uwsm.desktop}"
: "${DECKSHIFT_GAMING_SESSION_ID:=gamescope-session-steam-nm.desktop}"
# Flags that track whether the user needs to reboot or re-login after setup.
# Various steps set these to 1 when they make changes that only take effect
@@ -1321,10 +1324,10 @@ setup_settings_tui() {
# to seamlessly switch between Desktop Mode (Hyprland) and Gaming Mode
# (Gamescope + Steam Big Picture).
#
# The switching mechanism works through SDDM (the display/login manager):
# The switching mechanism works through a session backend:
# 1. User presses Super+Shift+S in Hyprland
# 2. switch-to-gaming script updates SDDM config to point to Gaming session
# 3. SDDM restarts and auto-logs into the Gaming Mode session
# 2. switch-to-gaming selects the Gaming Mode session
# 3. The backend transitions through SDDM or the configured greeter
# 4. gamescope-session-nm-wrapper starts performance tuning, NetworkManager,
# drive mounting, keybind monitor, then launches Gamescope + Steam
# 5. When done (Super+Shift+R or Steam > Exit to Desktop), the reverse happens
@@ -1335,7 +1338,7 @@ setup_settings_tui() {
# - Keybind monitor (Python daemon using evdev for Super+Shift+R)
# - NetworkManager start/stop scripts (iwd <-> NM handoff)
# - Steam library auto-mount daemon
# - SDDM session entry and config
# - Session entry, backend config, and display-manager helpers
# - Polkit and sudoers rules for passwordless operation
# - Hyprland keybind for Super+Shift+S
#
@@ -1431,6 +1434,50 @@ PY
info "Patched $gsp — CUSTOM_REFRESH_RATES now reaches gamescope via --nested-refresh"
}
detect_session_backend() {
case "${DECKSHIFT_SESSION_BACKEND}" in
sddm|dms-greeter-manual)
printf '%s\n' "$DECKSHIFT_SESSION_BACKEND"
return 0
;;
auto)
;;
*)
die "Unsupported DECKSHIFT_SESSION_BACKEND: $DECKSHIFT_SESSION_BACKEND"
;;
esac
local display_manager=""
if [[ -L /etc/systemd/system/display-manager.service ]]; then
display_manager=$(basename "$(readlink -f /etc/systemd/system/display-manager.service)" 2>/dev/null || true)
fi
if [[ "$display_manager" == "greetd.service" ]] &&
[[ -r /etc/greetd/config.toml ]] &&
grep -q 'dms-greeter' /etc/greetd/config.toml; then
printf '%s\n' "dms-greeter-manual"
return 0
fi
printf '%s\n' "sddm"
}
read_installed_session_backend() {
local conf="/etc/deckshift/session-backend.conf"
if [[ -r "$conf" ]]; then
local installed_backend
installed_backend=$(sed -n 's/^DECKSHIFT_SESSION_BACKEND=//p' "$conf" 2>/dev/null | head -1)
case "$installed_backend" in
sddm|dms-greeter-manual)
printf '%s\n' "$installed_backend"
return 0
;;
esac
fi
detect_session_backend
}
setup_session_switching() {
echo ""
echo "================================================================"
@@ -1471,6 +1518,10 @@ setup_session_switching() {
local current_user="${SUDO_USER:-$USER}"
local user_home
user_home=$(eval echo "~$current_user")
local session_backend
session_backend=$(detect_session_backend)
local gaming_session_path="/usr/share/wayland-sessions/${DECKSHIFT_GAMING_SESSION_ID}"
info "Using session backend: $session_backend"
# GPU detection only — the installer no longer chooses a monitor, resolution
# or refresh rate. Those are user choices, made later via Walker → "DeckShift
@@ -2285,14 +2336,13 @@ NM_WRAPPER
sudo chmod +x "$nm_wrapper"
info "Created $nm_wrapper"
# SDDM Session Entry
# Gaming Session Entry
#
# SDDM (the login/display manager) needs a .desktop file to know about
# Gaming Mode as a session option. This is what tells SDDM "when you
# auto-login to the gaming session, run this script." It's placed in
# /usr/share/wayland-sessions/ alongside the normal Hyprland session.
info "Creating SDDM session entry..."
local session_desktop="/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"
# Display managers and greeters need a .desktop file to know about Gaming
# Mode as a session option. The active session backend selects this entry
# when switching into Gaming Mode.
info "Creating Gaming Mode session entry..."
local session_desktop="$gaming_session_path"
sudo tee "$session_desktop" > /dev/null << 'SESSION_DESKTOP'
[Desktop Entry]
@@ -2310,19 +2360,19 @@ SESSION_DESKTOP
# When you click Steam > Power > "Exit to Desktop" inside Gaming Mode,
# Steam calls /usr/lib/os-session-select. On a real Steam Deck this
# switches to Desktop Mode. Our version does the same thing — it updates
# SDDM to boot back into Hyprland and restarts the display manager.
# selects the desktop session and asks the active backend to transition.
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 || {
sudo -n /usr/local/bin/deckshift-session-switch desktop 2>/dev/null || {
echo "Warning: Failed to update session config"
}
timeout 5 steam -shutdown 2>/dev/null || true
sleep 1
nohup sudo -n systemctl restart sddm &>/dev/null &
nohup /usr/local/bin/deckshift-session-transition desktop &>/dev/null &
disown
exit 0
OS_SESSION_SELECT
@@ -2335,10 +2385,10 @@ OS_SESSION_SELECT
# This script handles the transition from Desktop to Gaming Mode:
# 1. Masks suspend targets — prevents the system from sleeping when the
# monitor briefly disconnects during the display manager restart
# 2. Updates SDDM config to auto-login to the gaming session
# 2. Selects the gaming session through DeckShift's active backend
# 3. Kills any leftover gamescope processes from a previous session
# 4. Switches to VT2 (virtual terminal) to avoid display conflicts
# 5. Restarts SDDM, which auto-logs into Gaming Mode
# 5. Transitions through SDDM or the configured greeter
info "Creating switch-to-gaming script..."
local switch_script="/usr/local/bin/switch-to-gaming"
@@ -2346,7 +2396,7 @@ OS_SESSION_SELECT
#!/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 || {
sudo -n /usr/local/bin/deckshift-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
@@ -2377,7 +2427,7 @@ fi
sudo -n chvt 2 2>/dev/null || true
sleep 0.3
sudo -n systemctl restart sddm
/usr/local/bin/deckshift-session-transition gaming
SWITCH_SCRIPT
sudo chmod +x "$switch_script"
@@ -2390,8 +2440,8 @@ SWITCH_SCRIPT
# 2. Restores Bluetooth (disabled during gaming to reduce interference)
# 3. Gracefully shuts down Steam (timeout 5s, then force kill)
# 4. Kills gamescope with SIGTERM first, then SIGKILL if it won't die
# 5. Updates SDDM config back to Hyprland session
# 6. Restarts SDDM, which auto-logs into Desktop Mode
# 5. Selects the desktop session through DeckShift's active backend
# 6. Transitions through SDDM or the configured greeter
info "Creating switch-to-desktop script..."
local switch_desktop_script="/usr/local/bin/switch-to-desktop"
@@ -2429,7 +2479,7 @@ fi
sudo -n systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
sudo -n systemctl unmask --runtime sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null
sudo -n systemctl daemon-reload 2>/dev/null
sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true
sudo -n /usr/local/bin/deckshift-session-switch desktop 2>/dev/null || true
# Re-enable Bluetooth
sudo -n /usr/bin/rfkill unblock bluetooth 2>/dev/null || true
@@ -2465,9 +2515,10 @@ sudo -n chvt 2 2>/dev/null || true
sleep 0.5
# Atomic restart — stop+start (with stop and start as separate sudo calls)
# was unreliable: stop/start aren't NOPASSWD-allowed individually (only
# `restart` is), and the disowned `start` could be killed by session teardown
# before SDDM actually came back up, leaving the user on a black screen.
sudo -n systemctl restart sddm
# `restart` is for SDDM), and the disowned `start` could be killed by session
# teardown before the display manager actually came back up, leaving the user
# on a black screen.
/usr/local/bin/deckshift-session-transition desktop
exit 0
SWITCH_DESKTOP
@@ -2661,21 +2712,14 @@ HYPR_PORTAL
warn "Add manually: exec-once = /usr/local/bin/deckshift-portal-recovery"
fi
# SDDM Session Switching Config
# Session Backend Config
#
# SDDM supports auto-login — it can automatically log in a user to a
# specific session without showing the login screen. This config file
# controls WHICH session SDDM auto-logs into.
#
# The switching mechanism works by editing this file:
# - "Session=hyprland-uwsm" → boots into Desktop Mode
# - "Session=gamescope-session-steam-nm" → boots into Gaming Mode
#
# The gaming-session-switch helper script toggles this value, then SDDM
# is restarted to pick up the change. The "zz-" prefix ensures this
# config loads LAST and overrides any other SDDM autologin settings.
info "Creating SDDM session switching config..."
local sddm_gaming_conf="/etc/sddm.conf.d/zz-gaming-session.conf"
# Keep display-manager-specific behavior behind small helpers so DeckShift's
# Gamescope, Steam, GPU, and performance logic can stay backend-agnostic.
info "Creating session backend config..."
local deckshift_config_dir="/etc/deckshift"
local deckshift_backend_conf="${deckshift_config_dir}/session-backend.conf"
sudo install -d -m 0755 "$deckshift_config_dir"
local autologin_user="$current_user"
if [[ -f /etc/sddm.conf.d/autologin.conf ]]; then
@@ -2683,49 +2727,198 @@ HYPR_PORTAL
[[ -z "$autologin_user" ]] && autologin_user="$current_user"
fi
sudo tee "$deckshift_backend_conf" > /dev/null << BACKEND_CONF
DECKSHIFT_SESSION_BACKEND=${session_backend}
DECKSHIFT_DESKTOP_SESSION_ID=${DECKSHIFT_DESKTOP_SESSION_ID}
DECKSHIFT_GAMING_SESSION_ID=${DECKSHIFT_GAMING_SESSION_ID}
DECKSHIFT_GAMING_SESSION_PATH=${gaming_session_path}
DECKSHIFT_SDDM_CONF=/etc/sddm.conf.d/zz-gaming-session.conf
DECKSHIFT_SDDM_USER=${autologin_user}
DECKSHIFT_DMS_MEMORY_FILE=/var/cache/dms-greeter/.local/state/memory.json
DECKSHIFT_DMS_GREETER_GROUP=greeter
BACKEND_CONF
sudo chmod 0644 "$deckshift_backend_conf"
info "Created $deckshift_backend_conf"
if [[ "$session_backend" == "sddm" ]]; then
info "Creating SDDM session switching config..."
local sddm_gaming_conf="/etc/sddm.conf.d/zz-gaming-session.conf"
sudo install -d -m 0755 /etc/sddm.conf.d
sudo tee "$sddm_gaming_conf" > /dev/null << SDDM_GAMING
[Autologin]
User=${autologin_user}
Session=hyprland-uwsm
Session=${DECKSHIFT_DESKTOP_SESSION_ID%.desktop}
Relogin=true
SDDM_GAMING
info "Created $sddm_gaming_conf"
else
info "DMS greeter backend selected; SDDM autologin config will not be created"
fi
info "Creating session switching helper script..."
local session_helper="/usr/local/bin/gaming-session-switch"
info "Creating session switching helper scripts..."
local session_helper="/usr/local/bin/deckshift-session-switch"
sudo tee "$session_helper" > /dev/null << 'SESSION_HELPER'
#!/bin/bash
CONF="/etc/sddm.conf.d/zz-gaming-session.conf"
set -euo pipefail
CONF="/etc/deckshift/session-backend.conf"
if [[ ! -f "$CONF" ]]; then
echo "Error: Config file not found: $CONF" >&2
exit 1
fi
case "$1" in
gaming)
sed -i 's/^Session=.*/Session=gamescope-session-steam-nm/' "$CONF"
echo "Session set to: gaming mode"
;;
desktop)
sed -i 's/^Session=.*/Session=hyprland-uwsm/' "$CONF"
echo "Session set to: desktop mode"
;;
# shellcheck disable=SC1090
source "$CONF"
target="${1:-}"
case "$target" in
gaming|desktop) ;;
*)
echo "Usage: $0 {gaming|desktop}" >&2
exit 1
;;
esac
session_id_for_target() {
case "$1" in
gaming) printf '%s\n' "$DECKSHIFT_GAMING_SESSION_ID" ;;
desktop) printf '%s\n' "$DECKSHIFT_DESKTOP_SESSION_ID" ;;
esac
}
resolve_session_path() {
local session_id="$1"
local explicit=""
if [[ "$session_id" == "$DECKSHIFT_GAMING_SESSION_ID" ]]; then
explicit="${DECKSHIFT_GAMING_SESSION_PATH:-}"
fi
if [[ -n "$explicit" && -f "$explicit" ]]; then
printf '%s\n' "$explicit"
return 0
fi
local dir candidate
for dir in /usr/local/share/wayland-sessions /usr/share/wayland-sessions; do
candidate="${dir}/${session_id}"
if [[ -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
echo "Error: session file not found for ${session_id}" >&2
exit 1
}
write_dms_memory() {
local session_id="$1"
local session_path="$2"
local memory_file="${DECKSHIFT_DMS_MEMORY_FILE:-/var/cache/dms-greeter/.local/state/memory.json}"
local tmp
tmp=$(mktemp)
python3 - "$memory_file" "$session_path" "$session_id" > "$tmp" <<'PY'
import json
import os
import sys
path, session_path, session_id = sys.argv[1:4]
data = {}
try:
if os.path.exists(path):
with open(path) as fh:
loaded = json.load(fh)
if isinstance(loaded, dict):
data = loaded
except Exception:
data = {}
data["lastSessionId"] = session_path
data["lastSessionDesktopId"] = session_id
json.dump(data, sys.stdout, indent=2)
sys.stdout.write("\n")
PY
install -d -m 0775 "$(dirname "$memory_file")"
install -m 0664 "$tmp" "$memory_file"
rm -f "$tmp"
if getent group "${DECKSHIFT_DMS_GREETER_GROUP:-greeter}" >/dev/null 2>&1; then
chgrp "${DECKSHIFT_DMS_GREETER_GROUP:-greeter}" "$(dirname "$memory_file")" "$memory_file" || true
fi
}
session_id="$(session_id_for_target "$target")"
case "${DECKSHIFT_SESSION_BACKEND:-sddm}" in
sddm)
sddm_conf="${DECKSHIFT_SDDM_CONF:-/etc/sddm.conf.d/zz-gaming-session.conf}"
if [[ ! -f "$sddm_conf" ]]; then
echo "Error: SDDM config file not found: $sddm_conf" >&2
exit 1
fi
sed -i "s/^Session=.*/Session=${session_id%.desktop}/" "$sddm_conf"
;;
dms-greeter-manual)
session_path="$(resolve_session_path "$session_id")"
write_dms_memory "$session_id" "$session_path"
;;
*)
echo "Error: unsupported DeckShift session backend: ${DECKSHIFT_SESSION_BACKEND:-}" >&2
exit 1
;;
esac
echo "Session set to: ${target} mode"
SESSION_HELPER
sudo chmod +x "$session_helper"
info "Created $session_helper"
local legacy_session_helper="/usr/local/bin/gaming-session-switch"
sudo tee "$legacy_session_helper" > /dev/null << 'LEGACY_SESSION_HELPER'
#!/bin/bash
exec /usr/local/bin/deckshift-session-switch "$@"
LEGACY_SESSION_HELPER
sudo chmod +x "$legacy_session_helper"
info "Created compatibility wrapper $legacy_session_helper"
local transition_helper="/usr/local/bin/deckshift-session-transition"
sudo tee "$transition_helper" > /dev/null << 'TRANSITION_HELPER'
#!/bin/bash
set -euo pipefail
CONF="/etc/deckshift/session-backend.conf"
[[ -f "$CONF" ]] || { echo "Error: Config file not found: $CONF" >&2; exit 1; }
# shellcheck disable=SC1090
source "$CONF"
case "${DECKSHIFT_SESSION_BACKEND:-sddm}" in
sddm)
exec sudo -n systemctl restart sddm
;;
dms-greeter-manual)
if [[ -n "${XDG_SESSION_ID:-}" ]]; then
exec loginctl terminate-session "$XDG_SESSION_ID"
fi
exec sudo -n systemctl restart greetd.service
;;
*)
echo "Error: unsupported DeckShift session backend: ${DECKSHIFT_SESSION_BACKEND:-}" >&2
exit 1
;;
esac
TRANSITION_HELPER
sudo chmod +x "$transition_helper"
info "Created $transition_helper"
# Sudoers Rules for Session Switching
#
# The session switching scripts need to run several commands as root
# (restart SDDM, start/stop NetworkManager, control Bluetooth, etc.).
# (restart display managers, start/stop NetworkManager, control Bluetooth, etc.).
# These sudoers rules allow members of the "video" and "wheel" groups
# to run ONLY these specific commands without a password.
#
@@ -2744,7 +2937,10 @@ SESSION_HELPER
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/local/bin/deckshift-session-switch
%video ALL=(ALL) NOPASSWD: /usr/local/bin/deckshift-session-transition
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart sddm
%video ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart greetd.service
%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
@@ -2846,10 +3042,13 @@ HYPR_GAMING
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 " - ${gaming_session_path}"
echo " - /usr/lib/os-session-select"
echo " - /usr/local/bin/switch-to-gaming"
echo " - /usr/local/bin/switch-to-desktop"
echo " - /usr/local/bin/deckshift-session-switch"
echo " - /usr/local/bin/deckshift-session-transition"
echo " - /etc/deckshift/session-backend.conf (${session_backend})"
echo " - /usr/local/bin/gaming-keybind-monitor (Super+Shift+R)"
echo " - ~/.config/hypr/bindings.conf (keybind added)"
echo ""
@@ -2910,10 +3109,14 @@ verify_installation() {
local all_ok=true
local missing_files=()
local permission_issues=()
local verify_session_backend
verify_session_backend=$(read_installed_session_backend)
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/deckshift-session-switch"]="755:Backend-neutral session selector"
["/usr/local/bin/deckshift-session-transition"]="755:Backend-neutral display-manager transition helper"
["/usr/local/bin/gaming-session-switch"]="755:Session switching helper (gaming/desktop)"
["/usr/lib/os-session-select"]="755:Steam Exit to Desktop handler"
["/usr/local/bin/switch-to-gaming"]="755:Hyprland to Gaming Mode switcher"
@@ -2927,8 +3130,9 @@ verify_installation() {
["/usr/bin/steamos-update"]="755:Steam compatibility (from AUR package)"
["/usr/bin/jupiter-biosupdate"]="755:Steam compatibility (from AUR package)"
["/usr/bin/steamos-select-branch"]="755:Steam compatibility (from AUR package)"
["/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"]="644:SDDM session entry"
["/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"]="644:Gaming Mode Wayland session entry"
["/usr/share/gamescope-session-plus/gamescope-session-plus"]="755:ChimeraOS session launcher (from AUR)"
["/etc/deckshift/session-backend.conf"]="644:Session backend config"
["/etc/sddm.conf.d/zz-gaming-session.conf"]="644:SDDM session switching config"
["/etc/polkit-1/rules.d/50-gamescope-networkmanager.rules"]="644:Polkit NM rules"
["/etc/polkit-1/rules.d/50-udisks-gaming.rules"]="644:Polkit udisks2 rules (external drive mount)"
@@ -2953,7 +3157,16 @@ verify_installation() {
local description="${expected_files[$file]#*:}"
local is_optional=false
[[ "$description" == *"(optional)"* ]] && is_optional=true
case "$file" in
/etc/sddm.conf.d/zz-gaming-session.conf)
[[ "$verify_session_backend" == "dms-greeter-manual" ]] && is_optional=true
;;
/etc/NetworkManager/conf.d/10-iwd-backend.conf|\
/etc/NetworkManager/conf.d/20-unmanaged-systemd.conf|\
/usr/share/libalpm/hooks/deckshift-gamescope-cap.hook)
is_optional=true
;;
esac
if sudo test -f "$file" 2>/dev/null; then
local actual_perm
@@ -3112,6 +3325,48 @@ verify_installation() {
echo " systemd-networkd: $(systemctl is-active systemd-networkd.service 2>/dev/null || echo 'inactive')"
echo " polkit: $(systemctl is-active polkit.service 2>/dev/null || echo 'inactive')"
if [[ "$verify_session_backend" == "dms-greeter-manual" ]]; then
echo ""
echo " DMS GREETER BACKEND:"
echo " --------------------"
local dms_memory_file="/var/cache/dms-greeter/.local/state/memory.json"
if [[ -r /etc/deckshift/session-backend.conf ]]; then
local configured_memory_file
configured_memory_file=$(sed -n 's/^DECKSHIFT_DMS_MEMORY_FILE=//p' /etc/deckshift/session-backend.conf 2>/dev/null | head -1)
[[ -n "$configured_memory_file" ]] && dms_memory_file="$configured_memory_file"
fi
echo " Backend: $verify_session_backend"
echo " Memory file: $dms_memory_file"
if [[ -r "$dms_memory_file" ]]; then
local memory_stat
memory_stat=$(stat -c "%U:%G %a" "$dms_memory_file" 2>/dev/null || echo "unknown")
echo " ✓ DMS memory readable ($memory_stat)"
if python3 - "$dms_memory_file" <<'PY'
import json
import sys
with open(sys.argv[1]) as fh:
data = json.load(fh)
if not isinstance(data, dict):
raise SystemExit(1)
for key in ("lastSessionId", "lastSessionDesktopId"):
value = data.get(key)
if not isinstance(value, str) or not value:
raise SystemExit(1)
PY
then
echo " ✓ DMS memory has lastSessionId and lastSessionDesktopId"
else
echo " ✗ DMS memory JSON is missing expected session keys"
all_ok=false
fi
else
echo " ✗ DMS memory file is not readable"
all_ok=false
fi
fi
echo ""
echo " SUDO PERMISSIONS TEST:"
echo " ----------------------"
+423
View File
@@ -0,0 +1,423 @@
# DeckShift Session Backend Handoff
Last updated: 2026-07-11.
## Goal
Extend DeckShift so its Steam Deck-style gaming mode is not hard-wired to SDDM.
The first target is to preserve the existing SDDM behavior for upstream/Omarchy
users while adding a DMS/greetd manual backend for our Hyprland/DMS dotfiles.
The long-term goal is:
- `sddm`: existing seamless auto-login flow for Omarchy users.
- `dms-greeter-manual`: select the next session through DMS greeter memory and
require normal greeter authentication.
- Later: a host-gated one-shot auto-login backend for living-room machines, if
it can be made safe and recoverable.
## Repository State
Local checkout:
```text
/home/anodyine/Projects/deckshift
```
Current work branch:
```text
feature/session-backends
```
Remotes:
```text
origin ssh://git@tea.kyle-merritt.com:2222/anodyine/deckshift.git
github https://github.com/Anodyine/deckshift.git
upstream https://github.com/28allday/deckshift.git
```
`origin` is the Gitea working remote. `github` is the public fork. `upstream`
is fetch-only for the original DeckShift project.
## What Changed So Far
All code changes so far are in:
```text
deckshift.sh
README.md
```
### Installer Configuration
`deckshift.sh` now accepts these variables:
```sh
DECKSHIFT_SESSION_BACKEND=auto
DECKSHIFT_DESKTOP_SESSION_ID=hyprland-uwsm.desktop
DECKSHIFT_GAMING_SESSION_ID=gamescope-session-steam-nm.desktop
```
Supported backend values right now:
```text
auto
sddm
dms-greeter-manual
```
`auto` detects DMS/greetd when:
- `/etc/systemd/system/display-manager.service` resolves to `greetd.service`.
- `/etc/greetd/config.toml` contains `dms-greeter`.
Otherwise it falls back to `sddm`.
### Generated Backend Config
The installer writes:
```text
/etc/deckshift/session-backend.conf
```
It records the active backend, desktop/gaming session IDs, the Gamescope session
file path, SDDM config path, and DMS greeter memory path.
### Generated Helpers
The old SDDM-specific calls are now routed through backend-neutral helpers:
```text
/usr/local/bin/deckshift-session-switch
/usr/local/bin/deckshift-session-transition
```
`/usr/local/bin/gaming-session-switch` is still generated as a compatibility
wrapper that execs `deckshift-session-switch`.
Existing generated scripts now call the backend-neutral helpers:
```text
/usr/lib/os-session-select
/usr/local/bin/switch-to-gaming
/usr/local/bin/switch-to-desktop
```
### SDDM Backend Behavior
The SDDM backend preserves the previous model:
1. Write or update `/etc/sddm.conf.d/zz-gaming-session.conf`.
2. `deckshift-session-switch gaming|desktop` changes `Session=...`.
3. `deckshift-session-transition` runs `sudo -n systemctl restart sddm`.
This should be behaviorally equivalent to the original DeckShift flow.
### DMS Manual Backend Behavior
The DMS manual backend does not create SDDM config.
`deckshift-session-switch gaming|desktop`:
1. Resolves the target session file from an allowlist:
- desktop: `DECKSHIFT_DESKTOP_SESSION_ID`
- gaming: `DECKSHIFT_GAMING_SESSION_ID`
2. Looks in:
- `/usr/local/share/wayland-sessions`
- `/usr/share/wayland-sessions`
3. Writes DMS greeter memory:
```text
/var/cache/dms-greeter/.local/state/memory.json
```
4. Preserves any existing JSON keys and updates:
- `lastSessionId`
- `lastSessionDesktopId`
`deckshift-session-transition`:
1. If `XDG_SESSION_ID` exists, runs:
```sh
loginctl terminate-session "$XDG_SESSION_ID"
```
2. If there is no current session ID, falls back to:
```sh
sudo -n systemctl restart greetd.service
```
This intentionally returns the user to the DMS greeter for normal
authentication. It is not a one-shot auto-login implementation.
### Sudoers
The generated sudoers file now includes NOPASSWD entries for:
```text
/usr/local/bin/deckshift-session-switch
/usr/local/bin/deckshift-session-transition
/usr/bin/systemctl restart sddm
/usr/bin/systemctl restart greetd.service
```
The existing compatibility and NetworkManager/Bluetooth rules remain.
### README
The README now documents:
- The experimental DMS/greetd manual backend.
- The new backend config and helper files.
- The backend-neutral session switching flow.
- Updated uninstall cleanup for `/etc/deckshift` and the new helper scripts.
## Validation Already Run
These checks passed:
```sh
bash -n deckshift.sh
git diff --check
```
The generated heredoc scripts were also extracted and syntax-checked with
`bash -n`:
- `deckshift-session-switch`
- `deckshift-session-transition`
- `switch-to-gaming`
- `switch-to-desktop`
- `os-session-select`
- `gaming-session-switch` compatibility wrapper
`shellcheck` was not installed on this machine, so it has not been run yet.
No installer run has been performed. No live `/etc`, `/usr/local/bin`, greeter,
or display-manager state was changed by this work.
### DMS Greeter Selection Probe
After the first code pass, a manual DMS greeter selection test was run outside
this checkout. The greeter selected the expected `Deckshift dms test` session.
That confirms the core DMS manual backend assumption still holds: writing DMS
greeter memory can preselect a non-SDDM session for the next login. This does
not yet prove the full DeckShift-generated helper flow or Gamescope session
entry, but it is the right green light for implementing and testing the manual
DMS backend path before any auto-login work.
### DMS Generated Helper Probe
The generated helper path was then tested manually:
```sh
sudo -n /usr/local/bin/deckshift-session-switch gaming
/usr/local/bin/deckshift-session-transition gaming
```
It behaved as expected: the current session exited, DMS greeter appeared, and
the target gaming session was preselected.
This proves the first real DMS manual backend flow at the DeckShift helper
layer. Remaining DMS validation should focus on the full `switch-to-gaming`
script, Gamescope/Steam session startup, and the return path back to the
Hyprland UWSM desktop session.
### DMS Manual Round Trip
The DMS manual backend was also tested in both directions. It successfully
switched the greeter selection away from Hyprland UWSM and then back to the
Hyprland UWSM session. The user was able to log back into the UWSM desktop.
This proves the non-SDDM manual session-selection loop:
```text
Hyprland UWSM -> selected gaming/test session -> DMS greeter
DMS greeter/session -> selected Hyprland UWSM -> DMS greeter -> Hyprland UWSM
```
The next layer to test is no longer DMS memory/session preselection. It is the
real DeckShift gaming session: `switch-to-gaming`, Gamescope/Steam startup, and
`switch-to-desktop` return behavior.
## Important Constraints
Do not implement one-shot DMS auto-login yet. Manual DMS switching is the first
safe milestone.
Do not make this dotfiles-specific inside DeckShift. The DeckShift fork should
grow generic backend seams; the dotfiles repo can decide whether and how to
install/use the DMS backend.
Keep the SDDM behavior stable for existing users. The backend abstraction should
be boring and low-risk for the default Omarchy path.
## Known Gaps And Risks
### Not Tested Live
The backend helpers have only been syntax-checked. They still need live testing
on:
- An SDDM/Omarchy machine or VM.
- A DMS/greetd machine or VM.
- A real target host with Gamescope/Steam.
### DMS Memory Contract
The DMS greeter memory write is based on a local proof from the dotfiles repo:
writing `lastSessionId` and `lastSessionDesktopId` successfully preselected a
test session in DMS greeter.
Still verify:
- Ownership and permissions after DeckShift writes the file.
- Whether DMS ever overwrites the file during greeter startup.
- Whether preserving extra JSON keys is sufficient across DMS versions.
### Transition Semantics
For DMS manual mode, `loginctl terminate-session "$XDG_SESSION_ID"` is preferred.
The fallback `systemctl restart greetd.service` is more disruptive and should be
treated as a fallback only.
Verify whether `switch-to-desktop` from inside Gamescope has a valid
`XDG_SESSION_ID`. If not, returning from gaming may restart greetd.
### Session File Location
The installer currently writes the gaming session to:
```text
/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop
```
For our dotfiles, `/usr/local/share/wayland-sessions` may be a better long-term
owner boundary. Decide whether DeckShift should make this configurable or leave
the current upstream-compatible path alone.
### Verification Mode
`verify_installation` still treats `/etc/sddm.conf.d/zz-gaming-session.conf` as
optional by description only. The verification logic marks missing optional
files by checking whether the description contains `(optional)`, so this may
need adjustment if verifying a DMS backend install.
### Documentation Still Has Historical SDDM Mentions
Some README changelog and troubleshooting sections still mention SDDM because
they describe older releases or SDDM-specific recovery. That is probably fine,
but before publishing this branch upstream, review the README for places where
current behavior should say "session backend" instead.
## Recommended Next Steps
1. Install `shellcheck` on the dev machine or run it in a container/VM:
```sh
shellcheck deckshift.sh
shellcheck bin/deckshift-settings
```
2. Test SDDM backend generation without changing behavior:
```sh
DECKSHIFT_SESSION_BACKEND=sddm ./deckshift.sh
```
Verify:
- `/etc/deckshift/session-backend.conf`
- `/etc/sddm.conf.d/zz-gaming-session.conf`
- `/usr/local/bin/deckshift-session-switch`
- `/usr/local/bin/deckshift-session-transition`
- Existing Super+Shift+S / Super+Shift+R behavior.
3. Test DMS backend in the CachyOS/DMS VM or sacrificial host:
```sh
DECKSHIFT_SESSION_BACKEND=dms-greeter-manual ./deckshift.sh
```
Verify before switching:
- Gaming session appears in the DMS greeter session list.
- `deckshift-session-switch gaming` writes the expected DMS memory.
- `deckshift-session-switch desktop` restores the Hyprland UWSM session.
- File ownership/group/mode allow DMS greeter to read the memory file.
4. Test DMS manual transition without Steam first:
```sh
sudo -n /usr/local/bin/deckshift-session-switch gaming
/usr/local/bin/deckshift-session-transition gaming
```
Expected result:
- Current session exits.
- DMS greeter appears.
- Gaming session is preselected.
- User must authenticate manually.
5. Test return path:
```sh
sudo -n /usr/local/bin/deckshift-session-switch desktop
/usr/local/bin/deckshift-session-transition desktop
```
Expected result:
- Current session exits.
- DMS greeter appears.
- Hyprland UWSM is preselected.
6. Only after manual DMS mode works, test full Gamescope/Steam entry and return.
7. After both SDDM and DMS manual mode are proven, split large generated helper
bodies into clearer functions or files if desired. Avoid refactoring before
the behavior is proven.
8. Add Gitea issues for follow-up work:
- Prove SDDM backend parity.
- Prove DMS manual backend in VM.
- Fix verification mode for backend-specific expected files.
- Decide session file owner path.
- Prototype one-shot DMS/greetd auto-login separately.
## Recovery Notes For DMS Testing
If a DMS switch leaves the host at the wrong selected session, switch to a TTY
and write DMS memory back to Hyprland UWSM:
```sh
sudo python3 - <<'PY'
import json
path = "/var/cache/dms-greeter/.local/state/memory.json"
data = {}
try:
with open(path) as fh:
data = json.load(fh)
except Exception:
pass
data["lastSessionId"] = "/usr/local/share/wayland-sessions/hyprland-uwsm.desktop"
data["lastSessionDesktopId"] = "hyprland-uwsm.desktop"
with open(path, "w") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
PY
sudo chgrp greeter /var/cache/dms-greeter/.local/state /var/cache/dms-greeter/.local/state/memory.json 2>/dev/null || true
sudo chmod 0664 /var/cache/dms-greeter/.local/state/memory.json
sudo systemctl restart greetd.service
```
Do not rely on `chezmoi apply` as the only recovery path. The failure mode may
be that the graphical desktop is unavailable.