From 4b7725849fd90144812ef8ffa6586019275d7335 Mon Sep 17 00:00:00 2001 From: Kyle Merritt Date: Sat, 11 Jul 2026 17:48:08 -0400 Subject: [PATCH] added dms greeter auto session switch --- README.md | 38 ++- deckshift.sh | 371 +++++++++++++++++++++++----- docs/session-backend-handoff.md | 423 ++++++++++++++++++++++++++++++++ 3 files changed, 761 insertions(+), 71 deletions(-) create mode 100644 docs/session-backend-handoff.md diff --git a/README.md b/README.md index e429454..16adf5c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/deckshift.sh b/deckshift.sh index 08af101..73cc2d1 100755 --- a/deckshift.sh +++ b/deckshift.sh @@ -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 "$sddm_gaming_conf" > /dev/null << SDDM_GAMING + 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 "Created $sddm_gaming_conf" - - 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 " ----------------------" diff --git a/docs/session-backend-handoff.md b/docs/session-backend-handoff.md new file mode 100644 index 0000000..55515ec --- /dev/null +++ b/docs/session-backend-handoff.md @@ -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 +codex/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.