#!/bin/bash # # steamify.sh - Steamify CachyOS # # Wizard that makes a CachyOS (KDE Plasma) install behave like SteamOS: # boot into a Steam Deck-style gamescope session, switch to the Plasma # desktop and back, and reset to gamescope on the next boot/logout. # # It opens with a menu that detects which components are on and turns # them on or off to match what the user picks; turning one off restores # what was there before (system files from .bak-gamescope-wizard backups, # KDE settings from the undo journal in lib/state.sh). Components live in # lib/, see TECHNICAL.md. Safe to re-run. set -uo pipefail # Release version, see CHANGELOG.md. VERSION=2.11.1 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for lib in common valve-mirror state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh nvidia controllers cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier first-login menu backend; do # shellcheck source=/dev/null source "$SCRIPT_DIR/lib/$lib.sh" done require_root_helper # What this run does, from the first argument: # --backend ... the graphical app's machine-readable mode # --defaults [--options ,...] [--boot gamescope|desktop]: apply what the # menu would preselect (or exactly the listed items), without the menu or # any prompt (the Steam Machine ISO's first login runs this; sudo must not # ask) # --defaults --list what --defaults can set up here, as JSON # (an installer page is built from it); nothing else on stdout # --boot gamescope|desktop only change where an installed conversion # starts, everything else stays as it is (for scripts; no menu) # --first-login the installer's one-time follow-up # (none) the menu MODE=menu case "${1:-}" in --backend) MODE=backend ;; --defaults) MODE=defaults; [[ "${2:-}" == --list ]] && MODE=list ;; --boot) MODE=boot ;; --first-login) MODE=first-login ;; esac BACKEND=false; [[ "$MODE" == backend ]] && BACKEND=true if [[ "$MODE" != backend && "$MODE" != list ]]; then echo -e "${c_bold}Steamify CachyOS${c_reset} v$VERSION" echo "Turn the SteamOS-style parts on or off. The menu shows what is on now;" echo "anything you turn off is put back the way it was." fi if ! command -v pacman >/dev/null 2>&1; then err "This doesn't look like an Arch/CachyOS system (no pacman found). Aborting." exit 1 fi # Everything per-user (autologin user, Steam, theme, shortcut) is for the # user running the script. TARGET_USER="$(id -un)" migrate_layout notify_seen restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; } restart_now() { if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then warn "The BIOS update is written during this restart. Keep the power on and don't" warn "touch the machine until it has fully started again, even if the screen stays black." fi if [[ -n "${BIOS_DRY_RUN:-}" ]]; then ok "Dry run: would restart now (sudo reboot); not restarting." exit 0 fi info "Restarting..." sudo reboot exit 0 } after_run() { # After a run: back to the menu, or restart right away when something # that just ran needs it. Returns 1 when input has ended (scripted runs). local reply if ! restart_needed; then read -rp "Press Enter to go back to the menu... " _ || return 1 return 0 fi if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then warn "The BIOS update is installed at the next restart." else info "The login changes take effect after a restart." fi while true; do read -rp "$(echo -e "${c_bold}[m]${c_reset} back to the menu ${c_bold}[r]${c_reset} restart now: ")" reply || return 1 case "$reply" in m|M|"") return 0 ;; r|R) restart_now ;; *) warn "Type m or r." ;; esac done } quit_prompt() { # On q: when something needs a restart, offer it (default yes); "n" goes # back to the menu, and the next q asks again. Returns 0 to show the menu # again, 1 to quit (nothing to restart, or input ended in scripted runs). local reply restart_needed || return 1 echo if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then warn "The BIOS update is written during the next restart. Keep the power on and" warn "don't touch the machine until it has fully started again, even if the screen stays black." else info "The changes take effect after a restart." fi read -rp "$(echo -e "${c_bold}Restart now?${c_reset} [Y/n] (n = back to the menu, Ctrl+C = quit without restarting) ")" reply || return 1 case "$reply" in ""|y|Y) restart_now ;; esac return 0 } report_failed() { # Exit status for the scripted modes: 1 when a component had problems. [[ ${#FAILED[@]} -eq 0 ]] || { warn "These had problems (see above): ${FAILED[*]}"; return 1; } } run_defaults() { RESTART_FOR_LOGIN=false REAPPLY=false sudo -n true 2>/dev/null || { err "--defaults needs sudo without a password."; return 1; } detect_components defaults_options "${@:2}" || return 1 plan_changes apply_changes # Run by the installer: the rest waits for the first desktop login. user_session || first_login_schedule report_failed || return 1 ok "Done; the changes take effect after a restart." } run_boot_only() { local mode="${2:-}" c RESTART_FOR_LOGIN=false REAPPLY=false case "$mode" in gamescope|desktop) ;; *) err "--boot takes gamescope or desktop"; return 1 ;; esac detect_components [[ "${CURRENT[gaming]}" == 1 ]] || { err "--boot needs the SteamOS conversion; turn it on first."; return 1; } # What is on stays on, and nothing else changes: not the updates or # removals a normal run would pick. for c in "${COMPONENTS[@]}"; do WANTED[$c]=${CURRENT[$c]:-0}; done WANTED[boot]=0; [[ "$mode" == desktop ]] && WANTED[boot]=1 plan_changes if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then ok "Already starting in $mode." return 0 fi apply_changes report_failed || return 1 ok "Starts in $mode from the next boot on." } case "$MODE" in backend) # The graphical app (steamify-ui) drives the same components through # lib/backend.sh instead of the menu. RESTART_FOR_LOGIN=false shift backend_main "$@" exit $? ;; first-login) first_login_run; exit 0 ;; list) defaults_list; exit 0 ;; defaults) run_defaults "$@"; exit $? ;; boot) run_boot_only "$@"; exit $? ;; esac # Menu loop: after each run the menu comes back with the new state, until # the user quits; the restart question comes then, once, for everything. RESTART_FOR_LOGIN=false SUDO_KEEPALIVE=false while true; do detect_components if ! run_menu; then quit_prompt && continue break fi plan_changes if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then feature_record_unticked ok "Everything is already the way you want it." read -rp "Press Enter to go back to the menu... " _ || break continue fi echo echo -e "${c_bold}This will:${c_reset}" for c in "${TO_DISABLE[@]}"; do if [[ "$c" == boot ]]; then echo " - boot into: gamescope (from the next boot)" else echo " - turn off: ${LABEL[$c]}"; fi done for c in "${TO_ENABLE[@]}"; do if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)" elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)" elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)" elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}" elif feature_new "$c"; then echo " - turn on: ${LABEL[$c]} (new in this version)" else echo " - turn on: ${LABEL[$c]}"; fi done ask_yn "Go ahead?" y || { info "Nothing changed."; continue; } # Ask for the sudo password once and keep it fresh, instead of prompting # at random points during the run. sudo -n true 2>/dev/null || sudo -v || exit 1 if [[ "$SUDO_KEEPALIVE" == false ]]; then SUDO_KEEPALIVE=true while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & fi apply_changes # Login manager changes only take effect after a restart. [[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] && RESTART_FOR_LOGIN=true echo detect_components echo -e "${c_bold}Done. Current state:${c_reset}" for c in "${COMPONENTS[@]}"; do component_available "$c" && ! is_action "$c" || continue if [[ "$c" == boot ]]; then [[ "${CURRENT[gaming]}" == 1 ]] && echo " └ boots into: $(boot_mode "${CURRENT[boot]}")" elif [[ -n "${PARENT[$c]:-}" ]]; then [[ "${CURRENT[${PARENT[$c]}]}" == 1 ]] || continue if [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " └ ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " └ off ${LABEL[$c]}"; fi elif [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " off ${LABEL[$c]}"; fi done if [[ ${#FAILED[@]} -gt 0 ]]; then warn "These had problems (see above): ${FAILED[*]}" fi echo after_run || break done echo if restart_needed; then # Only reached when input ended (scripted runs): never restart then. info "Not restarted; the changes take effect after a restart." else info "Bye." fi