From e86ddcc4af8135c8e972a958294cecc26b19fd47 Mon Sep 17 00:00:00 2001 From: rickpeters Date: Sun, 27 Sep 2026 18:16:14 +0200 Subject: [PATCH] feat: Add as non-Steam game: Steamify in the Steam library, under the shortcut; controller works when started from Steam --- AGENTS.md | 9 +++ CHANGELOG.md | 8 ++ README.md | 4 + TECHNICAL.md | 30 +++++++ lib/menu.sh | 12 ++- lib/steam-game.sh | 129 ++++++++++++++++++++++++++++++ patches/README.md | 1 + patches/steam-shortcuts.py | 159 +++++++++++++++++++++++++++++++++++++ steamify.sh | 4 +- ui/qml/AppState.qml | 2 + ui/qml/Texts.qml | 3 + 11 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 lib/steam-game.sh create mode 100644 patches/steam-shortcuts.py diff --git a/AGENTS.md b/AGENTS.md index 5c7b7a8..dfed8b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -243,6 +243,15 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine `linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the compiler, never pass `LLVM=1`. Test shutdown on the real machine for every new major kernel. +- Add as non-Steam game (`steamgame`, `lib/steam-game.sh`, sub-option of + `launcher`, ticked along with it, 2.5.1): edits Steam's `shortcuts.vdf` + with `patches/steam-shortcuts.py` only while Steam is closed (Steam + rewrites it on exit), then starts Steam again. Never close Steam in gaming + mode or when `SteamGameId` is set (the run is a Steam game). An existing + entry for the start script is updated, not doubled; removal takes every + entry for it. Controller input through Steam's desktop layout arrives as + keys: Enter must select (A), Esc must never quit (B); the app re-execs + without Steam's overlay preload. - Update notifications (`notify`, `lib/update-notifier.sh`, top-level, ticked by default, 2.5.0): a user timer (daily, and the service is wanted by `plasma-workspace.target`) runs `patches/steamify-notifier.py`, which diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec9a6c..627cf9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes, per version and per commit. Versions follow `steamify.sh`. Versions before 0.7.0 were numbered afterwards, one per merged pull request. +## 2.5.1 - 2026-09-27 + +- **feat: Add as non-Steam game: Steamify in the Steam library, under the shortcut; controller works when started from Steam** +- `389bc33` **fix(ui): B never quits (Steam sends Esc for it), Ctrl+Q does; started from Steam, the hints show the Steam Controller's buttons** +- `c6c1150` **fix(ui): Enter selects (a Steam Controller's A in Steam's desktop layout); Ctrl+Enter reviews and applies** +- `98e2eff` **fix(ui): Started from Steam, the app runs without Steam's overlay (Qt aborted) and stays in the foreground, so Steam Input gives it the controller** +- `431515b` **feat(ui): Review & apply greyed out while nothing would change; the Done screen shows why a run stopped** + ## 2.5.0 - 2026-09-27 - **docs: README: what the VRAM booster says with NVIDIA's closed driver; shorter unsupported text in the app** diff --git a/README.md b/README.md index 703dc05..764a3dc 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ Steam Machine. 5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the launcher opens the newest Steamify app, so you don't need the install command again. "Steamify Terminal" in the launcher opens the terminal menu. + With **Add as non-Steam game** (on by default) it's in your Steam library + too: started from Steam it gets your controller (a Steam Controller only + talks to Steam), in gaming mode as well. Steam closes for a moment while + it's added, so that's done from the desktop. 6. **Update notifications** - on by default: a notification and a tray icon when there's a new Steamify, with Open Steamify and Skip this version. It never updates anything by itself diff --git a/TECHNICAL.md b/TECHNICAL.md index f08a416..657ee09 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -236,6 +236,35 @@ Where Steam Machine support was set up before 2.2.0, the menu ticks the power-off fix as a new default sub-option (see feature versions below), so a normal run adds it. +## Add as non-Steam game + +A sub-option of the Steamify shortcut (`steamgame`, `lib/steam-game.sh`, new +in 2.5.1, ticked along with the shortcut): Steamify CachyOS in every Steam +account's non-Steam games (`~/.local/share/Steam/userdata//config/shortcuts.vdf`, +binary VDF, edited by `patches/steam-shortcuts.py`), starting the shortcut's +`run-app`, with the icon rendered as PNG (`~/.local/share/steamify/steamify.png`; +Steam shows no SVG). Why: while Steam runs, a Steam Controller only talks to +Steam, which gives its input to the game it started (Steam Input); on the +desktop through its desktop layout (A = Enter, B = Esc, Y = Space, X = +Steam's keyboard), in gaming mode as a gamepad. + +Steam reads the list only at startup and writes it back on exit, so Steam is +closed (`steam -shutdown`) for the edit and started again (through +`steam-desktop-autostart.service` when it's there). That's refused in gaming +mode and when Steamify itself was started from Steam (`SteamGameId`), since +closing Steam would end it. An entry for the start script that's already +there (added by hand, or with the path from before 2.5.0) is updated, not +added twice; what was done is recorded per account (state `steamgame`: +`= added|updated`) and a copy of each list is kept in the +state directory before the first change. Turning it off (or the shortcut) +removes every entry for the start script, since it can't start anything +without it. + +Started from Steam, the app re-executes itself without Steam's overlay +(`LD_PRELOAD` of `gameoverlayrenderer.so` makes Qt abort) and +`steamify-app.sh` keeps it in the foreground (`SteamGameId`), or Steam +counts the game as ended right away and stops giving it the controller. + ## Update notifications The **Update notifications** option (new in 2.5.0, ticked by default) @@ -414,6 +443,7 @@ immediately, which can turn into a loop - see | `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository | | `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager | | `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) | +| `lib/steam-game.sh` | Add as non-Steam game: Steamify in the Steam library (`patches/steam-shortcuts.py`) | | `lib/update-notifier.sh` | Update notifications: the notifier from `patches/` and its user timer | | `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) | | `services/` | The systemd units the scripts install (`service_file`, `@KEY@` placeholders); see its README | diff --git a/lib/menu.sh b/lib/menu.sh index 21ebef5..e9b6200 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -5,12 +5,12 @@ # Menu order. Components are turned on in this order and off in reverse; # gaming must come first (single user builds on it). -COMPONENTS=(gaming boot theme glyphs single launcher notify vram cec machine poweroff kpin hdmi bios) +COMPONENTS=(gaming boot theme glyphs single launcher steamgame notify vram cec machine poweroff kpin hdmi bios) # One-off actions rather than on/off components: never preselected, never # re-applied, not listed as on or off. ACTIONS=(bios) # Sub-options, shown indented under their parent and only while it's ticked. -declare -A PARENT=([boot]=gaming [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine) +declare -A PARENT=([boot]=gaming [steamgame]=launcher [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine) # Never preselected on a first run: booting into the desktop is a choice, # gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or # upset other devices on the TV, even on SteamOS), except on a Steam Machine, @@ -26,7 +26,7 @@ NO_PRESELECT=(boot cec kpin hdmi) FEATURE_BASELINE=2.1.0 declare -A FEATURE_VERSION=( [gaming]=2.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 - [launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 + [launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [kpin]=2.1.0 [hdmi]=2.1.0 ) @@ -37,6 +37,7 @@ declare -A LABEL=( [glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode" [single]="Single user mode: no password, lock screen or log out (SDDM)" [launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher" + [steamgame]="Add as non-Steam game: Steamify in your Steam library, for the controller and gaming mode" [notify]="Update notifications: a notification when there's a new Steamify, never updates by itself" [vram]="VRAM booster: the game in front keeps its VRAM, background apps make room" [cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)" @@ -52,6 +53,7 @@ component_available() { case "$1" in machine|poweroff) machine_available ;; vram) vram_available ;; + steamgame) steamgame_available ;; kpin) kpin_available ;; hdmi) hdmi_available ;; bios) bios_available ;; @@ -192,6 +194,10 @@ toggle_component() { if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; WANTED[bios]=0; fi if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi if [[ "$c" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi + # Steam starts Steamify through the shortcut's start script. + # Opt-out, like the power-off fix: ticked along with the shortcut. + if [[ "$c" == launcher ]] && component_available steamgame; then WANTED[steamgame]=${WANTED[launcher]}; fi + if [[ "$c" == steamgame && "${WANTED[steamgame]}" == 1 ]]; then WANTED[launcher]=1; fi } show_menu() { diff --git a/lib/steam-game.sh b/lib/steam-game.sh new file mode 100644 index 0000000..d7544a5 --- /dev/null +++ b/lib/steam-game.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# "Add as non-Steam game" (steamgame), a sub-option of the Steamify shortcut: +# Steamify CachyOS in the Steam library, so Steam can start it: with Steam +# Input it then gets the controller (a Steam Controller only talks to Steam +# while Steam runs), in gaming mode too. Steam reads its non-Steam games +# (userdata//config/shortcuts.vdf, binary; patches/steam-shortcuts.py) +# only at startup and writes them back on exit, so Steam is closed for the +# edit and started again. That's refused while a Steam session depends on it: +# in gaming mode, or with this run started from Steam itself. What it did is +# recorded per Steam account (state steamgame, "": " added" +# or " updated", for an entry that was there already): Steam rewrites +# the file on exit, so a mark inside the entry could be lost; the start +# script's path in it is the other sign it's ours. +# Sourced by steamify.sh; not meant to be run on its own. + +STEAM_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/Steam" +STEAMGAME_NAME="Steamify CachyOS" +# Steam shows no SVG icons: the shortcut's icon rendered as PNG. +STEAMGAME_ICON="$STEAMIFY_DATA/steamify.png" + +steamgame_files() { + # Every Steam account's shortcuts.vdf (the file may not exist yet). + local d + for d in "$STEAM_ROOT"/userdata/*/config; do + [[ -d "$d" && "$(basename "$(dirname "$d")")" != 0 ]] && echo "$d/shortcuts.vdf" + done +} + +steamgame_tool() { + python3 <(patch_file steam-shortcuts.py) "$@" +} + +steamgame_available() { [[ -n "$(steamgame_files)" ]]; } + +steamgame_status() { + local f + while IFS= read -r f; do + [[ -f "$f" ]] && steamgame_tool find "$f" "$WIZARD_APP_LAUNCHER" >/dev/null && return 0 + done < <(steamgame_files) + return 1 +} + +steamgame_steam_busy() { + # Closing Steam would end what's running: gaming mode is Steam's own + # session, and a run started from Steam is one of its games. + if [[ -n "${SteamGameId:-}" ]]; then + err "Steamify was started from Steam, which has to close for this. Start it from the desktop icon, or run Steamify Terminal." + return 0 + fi + if pgrep -x gamescope >/dev/null || pgrep -f gamescope-session >/dev/null; then + err "Steam has to close for this, which would end gaming mode. Do it from the desktop." + return 0 + fi + return 1 +} + +steamgame_edit() { + # steamgame_edit : runs it with Steam closed, then starts Steam + # again if it was running (through our autostart unit when that's there). + local was_running=false i + if pgrep -x steam >/dev/null; then + steamgame_steam_busy && return 1 + was_running=true + info "Closing Steam for a moment (it only reads its games list at startup)..." + steam -shutdown >/dev/null 2>&1 + for ((i = 0; i < 60; i++)); do pgrep -x steam >/dev/null || break; sleep 1; done + pgrep -x steam >/dev/null && { err "Steam didn't close; nothing changed."; return 1; } + fi + "$1"; local rc=$? + if [[ "$was_running" == true ]]; then + if systemctl --user is-enabled -q steam-desktop-autostart.service 2>/dev/null; then + systemctl --user start steam-desktop-autostart.service + else + systemd-run --user --collect -q steam -silent >/dev/null 2>&1 + fi + info "Steam is starting again." + fi + return "$rc" +} + +steamgame_icon() { + # The PNG, or the SVG when it can't be rendered (librsvg comes with KDE). + if [[ -f "$WIZARD_ICON" ]] && command -v rsvg-convert >/dev/null && + mkdir -p "$STEAMIFY_DATA" && rsvg-convert -w 256 -h 256 -o "$STEAMGAME_ICON" "$WIZARD_ICON" 2>/dev/null; then + echo "$STEAMGAME_ICON" + else + echo "$WIZARD_ICON" + fi +} + +steamgame_add_all() { + local f out icon + icon="$(steamgame_icon)" + while IFS= read -r f; do + # A copy of each account's list before the first change. + [[ -f "$f" && ! -f "$STATE_DIR/shortcuts-$(basename "$(dirname "$(dirname "$f")")").vdf" ]] && + mkdir -p "$STATE_DIR" && cp -a "$f" "$STATE_DIR/shortcuts-$(basename "$(dirname "$(dirname "$f")")").vdf" + out="$(steamgame_tool add "$f" "$STEAMGAME_NAME" "$WIZARD_APP_LAUNCHER" "$icon" "$WIZARD_APP_ENTRY")" || + { err "Couldn't add Steamify to $f."; return 1; } + state_set steamgame "$(basename "$(dirname "$(dirname "$f")")")" "${out#* } ${out%% *}" + info "Steam library ($(basename "$(dirname "$(dirname "$f")")")): ${out%% *} $STEAMGAME_NAME." + done < <(steamgame_files) +} + +steamgame_remove_all() { + local f id + while IFS= read -r f; do + [[ -f "$f" ]] || continue + # Any entry for our start script: it can't start anything without it. + while id="$(steamgame_tool find "$f" "$WIZARD_APP_LAUNCHER")"; do + steamgame_tool remove "$f" "$id" || return 1 + done + done < <(steamgame_files) +} + +steamgame_enable() { + [[ -x "$WIZARD_APP_LAUNCHER" ]] || { err "The Steamify shortcut isn't set up; turn that on first."; return 1; } + info "Adding $STEAMGAME_NAME to your Steam library..." + steamgame_edit steamgame_add_all || return 1 + ok "Steamify is in your Steam library (non-Steam games): start it from there for the controller, in gaming mode too." +} + +steamgame_disable() { + info "Removing $STEAMGAME_NAME from your Steam library..." + steamgame_edit steamgame_remove_all || return 1 + state_clear steamgame + rm -f "$STEAMGAME_ICON" + ok "Steamify removed from your Steam library." +} diff --git a/patches/README.md b/patches/README.md index bf6c056..ffed613 100644 --- a/patches/README.md +++ b/patches/README.md @@ -11,5 +11,6 @@ read and reviewed on their own. `lib/*.sh` reads them with `patch_file | `ensure-kernel-headers.sh` | `lib/steam-machine.sh` (Steam Machine support) | Run at boot: installs missing headers for every installed kernel | | `leds-valve-dkms.conf` | `lib/steam-machine.sh` (Steam Machine support) | `/etc/dkms` override: build the LED driver for DKMS's target kernel; appended to someone's own override, which is backed up and restored | | `steamify-fremont-poweroff.dkms.conf`, `steamify-cros-ec-cec.dkms.conf` | `lib/fremont-poweroff.sh`, `lib/cec.sh` | The DKMS modules' `dkms.conf` (`fill NAME=... VERSION=...`) | +| `steam-shortcuts.py` | `lib/steam-game.sh` (Add as non-Steam game) | Reads and writes Steam's binary `shortcuts.vdf` (find, add or update, remove) | | `steamify-notifier.py` | `lib/update-notifier.sh` (Update notifications) | The daily check: notification and tray icon when there's a new release | | `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port | diff --git a/patches/steam-shortcuts.py b/patches/steam-shortcuts.py new file mode 100644 index 0000000..368cc35 --- /dev/null +++ b/patches/steam-shortcuts.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# Steamify CachyOS: Steam's non-Steam games list (userdata//config/ +# shortcuts.vdf, binary VDF), for "Add as non-Steam game". Steam reads it only +# at startup and writes it back on exit, so it's edited while Steam is closed. +# steam-shortcuts.py find -> appid of an entry for exe +# steam-shortcuts.py add +# -> "added " / "updated " +# steam-shortcuts.py remove +# An entry counts as ours when its Exe is , or a Steamify start script +# (run-app, also under the pre-2.5.0 path): that one is updated, not doubled. +import os +import struct +import sys +import zlib + +MAP, STR, INT, END = 0, 1, 2, 8 + + +def parse(data): + pos = 0 + + def cstr(): + nonlocal pos + end = data.index(b"\0", pos) + s = data[pos:end].decode("utf-8", "surrogateescape") + pos = end + 1 + return s + + def block(): + nonlocal pos + items = [] + while True: + t = data[pos] + pos += 1 + if t == END: + return items + key = cstr() + if t == MAP: + items.append((key, block())) + elif t == STR: + items.append((key, cstr())) + elif t == INT: + items.append((key, struct.unpack_from("