#!/bin/bash # Machine-readable mode for the graphical app (steamify-ui): the same # components and plan as the menu, as JSON on stdout. # steamify.sh --backend status # one JSON object: version, machine, and every menu item with its # current state and what a run would pick by default. # steamify.sh --backend apply [--reapply] [--boot gamescope|desktop] ... # turns on the listed components and off the others (like ticking them # in the menu), then streams one JSON object per line: plan, start, # log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal). # steamify.sh --backend bios-prepare | bios-flash # the BIOS update in two steps, so the app shows both warnings between # them (bios-ready event: current, newest, checksum, compatible). # Sourced by steamify.sh; not meant to be run on its own. json_str() { # json_str : as a JSON string (quotes, backslashes and # control characters escaped; colour codes dropped). local s="$1" s="$(printf '%s' "$s" | sed -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g')" s="${s//\\/\\\\}"; s="${s//\"/\\\"}" s="${s//$'\t'/\\t}"; s="${s//$'\r'/}"; s="${s//$'\n'/\\n}" s="$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037')" printf '"%s"' "$s" } json_list() { # json_list ...: a JSON array of strings. local out="" x for x in "$@"; do out+="${out:+,}$(json_str "$x")"; done printf '[%s]' "$out" } backend_event() { # backend_event [] printf '{"event":%s%s}\n' "$(json_str "$1")" "${2:+,$2}" } backend_status() { local c first_run=true items="" kind parent now detect_components for c in "${COMPONENTS[@]}"; do component_available "$c" && ! is_action "$c" && [[ "${CURRENT[$c]}" == 1 ]] && first_run=false done for c in "${COMPONENTS[@]}"; do component_available "$c" || continue kind=toggle; is_action "$c" && kind=action; [[ "$c" == boot ]] && kind=choice parent="${PARENT[$c]:-}" now="${CURRENT[$c]:-0}" items+="${items:+,}{\"id\":$(json_str "$c"),\"label\":$(json_str "${LABEL[$c]%%:*}")" items+=",\"hint\":$(json_str "$( [[ "${LABEL[$c]}" == *:* ]] && echo "${LABEL[$c]#*: }")")" items+=",\"kind\":\"$kind\",\"parent\":$(json_str "$parent")" items+=",\"needs\":\"\"" items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)" items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 1 ]] && echo true || echo false)" items+=",\"update\":$(feature_outdated "$c" && echo true || echo false)" items+=",\"new\":$({ feature_new "$c" || feature_new_optin "$c"; } && echo true || echo false)" items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)" # Why it can't be turned on, for the app's explanation. [[ "$c" == vram && -n "${VRAM_NVIDIA_CASE:-}" ]] && items+=",\"note\":$(json_str "$(vram_nvidia_note "$VRAM_NVIDIA_CASE")")" items+="}" done local cec="" f for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done local bios='null' if bios_available; then bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(bios_selectable && echo true || echo false),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}" fi printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \ "$(json_str "$VERSION")" "$first_run" \ "$(detect_valve_fremont && echo true || echo false)" \ "$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \ "$(json_str "$cec")" \ "$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \ "$bios" "$items" } backend_run_component() { # backend_run_component : runs it in this shell # (its variables, e.g. RESTART_FOR_LOGIN, must survive), each output line # as a log event. local c="$1" what="$2" rc backend_event start "\"id\":$(json_str "$c"),\"action\":\"$what\",\"label\":$(json_str "${LABEL[$c]%%:*}")" "${c}_${what}" > >(while IFS= read -r line; do backend_event log "\"id\":$(json_str "$c"),\"line\":$(json_str "$line")"; done) 2>&1 rc=$? # Let the log reader finish before the next event. wait $! 2>/dev/null backend_event "done" "\"id\":$(json_str "$c"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" return $rc } backend_sudo() { # sudo from the app has no terminal: ask through its helper, and keep the # credentials fresh as the menu does. if [[ -z "${SUDO_ASKPASS:-}" ]] && ! sudo -n true 2>/dev/null; then backend_event finished '"failed":[],"restart":false,"error":"no-sudo"' return 1 fi sudo() { command sudo ${SUDO_ASKPASS:+-A} "$@"; } if ! sudo -n true 2>/dev/null && ! sudo true 2>/dev/null; then backend_event finished '"failed":[],"restart":false,"error":"wrong-password"' return 1 fi while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & } backend_bios_prepare() { # Download, checksum and fwupd's device check; the app then shows its # two warnings and calls bios-flash. local rc backend_sudo || return 1 backend_event start '"id":"bios","action":"prepare","label":"Update BIOS"' bios_prepare > >(while IFS= read -r line; do backend_event log "\"id\":\"bios\",\"line\":$(json_str "$line")"; done) 2>&1 rc=$?; wait $! 2>/dev/null case $rc in 0) backend_event bios-ready "\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "$BIOS_NEWEST"),\"checksum\":true,\"compatible\":$(json_str "$BIOS_COMPATIBLE")" ;; 2) backend_event finished '"failed":[],"restart":false,"nothing":true' ;; *) backend_event finished '"failed":["bios"],"restart":false' ;; esac } backend_bios_flash() { backend_sudo || return 1 local rc backend_run_component bios flash_only; rc=$? backend_event finished "\"failed\":$([[ $rc -eq 0 ]] && echo '[]' || echo '["bios"]'),\"restart\":$(restart_needed && echo true || echo false),\"bios\":true" } backend_apply() { local reapply=false boot="" id c local -a want=() while [[ $# -gt 0 ]]; do case "$1" in --reapply) reapply=true ;; --boot) boot="$2"; shift ;; *) want+=("$1") ;; esac shift done detect_components # What the app ticked: exactly these on, every other component off. for c in "${COMPONENTS[@]}"; do component_available "$c" || continue is_action "$c" && { WANTED[$c]=0; continue; } WANTED[$c]=0 for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done done normalize_wanted # Retired: only ever removed. WANTED[hdmi]=0 case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac REAPPLY=$reapply plan_changes backend_event plan "\"disable\":$(json_list "${TO_DISABLE[@]}"),\"enable\":$(json_list "${TO_ENABLE[@]}")" if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then feature_record_unticked backend_event finished '"failed":[],"restart":false,"nothing":true' return 0 fi backend_sudo || return 1 LOGIN_MANAGER=plasmalogin [[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm local -a failed=() for c in "${TO_DISABLE[@]}"; do if backend_run_component "$c" disable; then feature_record "$c" disable; else failed+=("$c"); fi done for c in "${TO_ENABLE[@]}"; do if backend_run_component "$c" enable; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi done feature_record_unticked os_version_refresh [[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] && RESTART_FOR_LOGIN=true backend_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)" } backend_main() { case "${1:-}" in status) backend_status ;; apply) shift; backend_apply "$@" ;; bios-prepare) backend_bios_prepare ;; bios-flash) backend_bios_flash ;; *) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] ..."; return 2 ;; esac }