diff --git a/.github/tools/bundle.sh b/.github/tools/bundle.sh index 5812723..31b5ad2 100755 --- a/.github/tools/bundle.sh +++ b/.github/tools/bundle.sh @@ -31,23 +31,28 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh" echo done - # patches/ embedded: replaces lib/common.sh's patch_file, which reads - # them from the checkout. - echo "# ----- patches/ -----" - echo "patch_file() {" - echo ' case "$1" in' - for f in patches/*; do - [[ "$f" == patches/README.md ]] && continue - grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; } - echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'" - cat "$f" - echo "STEAMIFY_PATCH_EOF" - echo " ;;" - done - echo ' *) return 1 ;;' - echo ' esac' - echo "}" - echo + # patches/ and services/ embedded: replace lib/common.sh's patch_file + # and service_raw, which read them from the checkout. + embed() { + local fn="$1" dir="$2" f + echo "# ----- $dir/ -----" + echo "$fn() {" + echo ' case "$1" in' + for f in "$dir"/*; do + [[ "$f" == "$dir/README.md" ]] && continue + grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; } + echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'" + cat "$f" + echo "STEAMIFY_PATCH_EOF" + echo " ;;" + done + echo ' *) return 1 ;;' + echo ' esac' + echo "}" + echo + } + embed patch_file patches + embed service_raw services echo "# ----- steamify.sh -----" echo "main() {" # Everything after the source loop is the entry point's body. diff --git a/.github/workflows/bundle.yml b/.github/workflows/bundle.yml index 9700290..a057cf5 100644 --- a/.github/workflows/bundle.yml +++ b/.github/workflows/bundle.yml @@ -6,7 +6,7 @@ # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/latest/steamify.sh | bash # Each release also has the app (v2): steamify-app.tar.gz (ui/, steamify.sh, -# lib/, patches/, the icon) and its launcher, run with: +# lib/, patches/, services/, the icon) and its launcher, run with: # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash # A push to feat/v2-ui (the app, not released yet) updates the pre-release # v2-ui-preview instead, whose launcher gets that build: @@ -36,7 +36,7 @@ jobs: .github/tools/bundle.sh dist/steamify.sh # The app: sources, not the single-file build (its backend mode runs # steamify.sh from its own folder). - tar -czf dist/steamify-app.tar.gz steamify.sh lib patches ui assets/steam-gaming-settings.svg LICENSE.md + tar -czf dist/steamify-app.tar.gz steamify.sh lib patches services ui assets/steam-gaming-settings.svg LICENSE.md cp ui/steamify-app.sh dist/steamify-app.sh - name: Check diff --git a/AGENTS.md b/AGENTS.md index 13477b4..75500d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,12 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine apply, never inline in the shell code. Read them with `patch_file ` (`lib/common.sh`, from the checkout); the bundle embeds every file in `patches/` and overrides `patch_file`. Add new ones to its README. +- `services/` - the systemd units the scripts install (`.service`, + `.timer`, `.path`), never inline in the shell code. Read them with + `service_file [KEY=value...]` (`lib/common.sh`), which fills in + `@KEY@` placeholders; the bundle embeds every file in `services/` too + (`service_raw`). Add new ones to its README. Small drop-ins for other + packages' units stay inline. - `ui/` - the app: `steamify-ui` (PySide6; runs `steamify.sh --backend`, `lib/backend.sh`) and `ui/qml/`: `Main.qml` (window, header, which screen shows), `AppState.qml` (all state and logic, input actions), one diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fc536..64fe679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ one per merged pull request. ## 2.5.0 - 2026-09-27 -- **docs: README screenshot at 2.5.0 with Update notifications; AGENTS.md: retake it when rows change** +- **refactor: systemd units in services/, read with service_file like patches/** +- `8a2601d` **docs: README screenshot at 2.5.0 with Update notifications; AGENTS.md: retake it when rows change** - `572417b` **fix: Open Steamify from the notification starts the app (own scope), the same way it was last used (app or terminal)** - `cf73229` **feat: Update notifications: a notification and tray icon when there's a new Steamify** diff --git a/TECHNICAL.md b/TECHNICAL.md index 585bbc6..f9114c1 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -380,8 +380,9 @@ immediately, which can turn into a loop - see | `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) | | `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 | | `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README | -| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` embedded | +| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` and `services/` embedded | | `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` | The single-file version is generated: on every push to `main`, GitHub diff --git a/lib/boot-session.sh b/lib/boot-session.sh index 5e5d565..3c86836 100644 --- a/lib/boot-session.sh +++ b/lib/boot-session.sh @@ -23,19 +23,7 @@ boot_enable() { # the session into the base config; run it right away rather than rely # on its path unit, which could still be pending when the login manager # starts. - sudo tee "$BOOT_UNIT" > /dev/null << EOF -[Unit] -Description=Steamify CachyOS: boot into the desktop instead of gaming mode -Before=display-manager.service sddm.service plasmalogin.service - -[Service] -Type=oneshot -ExecStart=/usr/lib/steamos/steam-set-session plasma.desktop -ExecStart=/bin/sh -c '[ -x $SESSION_SYNC ] && exec $SESSION_SYNC || true' - -[Install] -WantedBy=graphical.target -EOF + service_file "$BOOT_UNIT_NAME" SESSION_SYNC="$SESSION_SYNC" | sudo tee "$BOOT_UNIT" > /dev/null sudo systemctl daemon-reload sudo systemctl enable "$BOOT_UNIT_NAME" || { err "Enabling $BOOT_UNIT_NAME failed."; return 1; } ok "Boots into the desktop from the next boot on (Return to Gaming Mode still works)." diff --git a/lib/common.sh b/lib/common.sh index dfcf60e..ba0c45c 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -9,6 +9,17 @@ ok() { echo -e "${c_green}[OK]${c_reset} $*"; } # patch_file : a file from patches/ (module sources, patches). The # single-file build replaces this with one that has them embedded. patch_file() { cat "$SCRIPT_DIR/patches/$1"; } +# service_file [KEY=value...]: a systemd unit from services/, with +# each @KEY@ replaced by its value. The single-file build replaces +# service_raw like patch_file. +service_raw() { cat "$SCRIPT_DIR/services/$1"; } +service_file() { + local name="$1" kv + local -a subst=(-e '') + shift + for kv in "$@"; do subst+=(-e "s|@${kv%%=*}@|${kv#*=}|g"); done + service_raw "$name" | sed "${subst[@]}" +} warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; } err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; } ask_yn() { diff --git a/lib/login-manager.sh b/lib/login-manager.sh index ad81b51..4cfde87 100644 --- a/lib/login-manager.sh +++ b/lib/login-manager.sh @@ -211,28 +211,9 @@ EOF # 7. systemd path watcher + service info "Installing systemd path watcher so session switches take effect immediately" - sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null << 'EOF' -[Unit] -Description=Watch for steamos session changes + service_file sync-steamos-session.path | sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null -[Path] -PathModified=/etc/plasmalogin.conf.d/zz-steamos-autologin.conf - -[Install] -WantedBy=multi-user.target -EOF - - sudo tee /etc/systemd/system/sync-steamos-session.service > /dev/null << 'EOF' -[Unit] -Description=Sync steamos session selection into plasmalogin.conf -# Session switches can come in bursts (switch + autologin reset); never -# let systemd's start rate limit silently stop the bridge. -StartLimitIntervalSec=0 - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/sync-steamos-session.sh -EOF + service_file sync-steamos-session.service | sudo tee /etc/systemd/system/sync-steamos-session.service > /dev/null sudo systemctl daemon-reload sudo systemctl enable --now sync-steamos-session.path diff --git a/lib/steam-desktop.sh b/lib/steam-desktop.sh index 5284c73..a240f2f 100644 --- a/lib/steam-desktop.sh +++ b/lib/steam-desktop.sh @@ -16,23 +16,7 @@ steam_enable() { systemd_user_dir="$HOME/.config/systemd/user" mkdir -p "$systemd_user_dir" - cat << 'EOF' > "$systemd_user_dir/steam-desktop-autostart.service" -[Unit] -Description=Steam Background Autostart for Virtual Keyboard -After=graphical-session.target -PartOf=graphical-session.target - -[Service] -Type=simple -# Only in the Plasma desktop session; gamescope-session starts its own Steam. -ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]' -ExecStart=/usr/bin/steam -silent -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=graphical-session.target -EOF + service_file steam-desktop-autostart.service > "$systemd_user_dir/steam-desktop-autostart.service" # Clean out old .desktop shortcut so they don't fight rm -f "$HOME/.config/autostart/steam.desktop" diff --git a/lib/steam-machine.sh b/lib/steam-machine.sh index 6fa43ff..0758682 100644 --- a/lib/steam-machine.sh +++ b/lib/steam-machine.sh @@ -312,19 +312,7 @@ echo "Installing missing kernel headers: ${missing[*]}" exec pacman -S --needed --noconfirm "${missing[@]}" EOF sudo chmod 755 "$HEADERS_SCRIPT" - sudo tee "$HEADERS_UNIT" > /dev/null << EOF -[Unit] -Description=Install missing kernel headers so DKMS builds leds-valve for every kernel -Wants=network-online.target -After=network-online.target - -[Service] -Type=oneshot -ExecStart=$HEADERS_SCRIPT - -[Install] -WantedBy=multi-user.target -EOF + service_file ensure-kernel-headers.service HEADERS_SCRIPT="$HEADERS_SCRIPT" | sudo tee "$HEADERS_UNIT" > /dev/null sudo systemctl daemon-reload sudo systemctl enable ensure-kernel-headers.service } diff --git a/lib/update-notifier.sh b/lib/update-notifier.sh index ce7d6b5..bf2a68d 100644 --- a/lib/update-notifier.sh +++ b/lib/update-notifier.sh @@ -39,34 +39,8 @@ notify_enable() { patch_file steamify-notifier.py | install_executable "$NOTIFY_SCRIPT" 755 || { err "Couldn't write $NOTIFY_SCRIPT."; return 1; } mkdir -p "$NOTIFY_UNITS" - # PartOf the graphical session: the tray icon goes with a logout. The - # short wait at login lets the tray come up first (in the script: an - # ExecStartPre sleep runs into the start timeout). - cat > "$NOTIFY_UNITS/$NOTIFY_NAME.service" << EOF -[Unit] -Description=Steamify: check for a new release -After=plasma-workspace.target -PartOf=graphical-session.target - -[Service] -Type=simple -ExecStart=$NOTIFY_SCRIPT --delay 60 - -[Install] -WantedBy=plasma-workspace.target -EOF - cat > "$NOTIFY_UNITS/$NOTIFY_NAME.timer" << EOF -[Unit] -Description=Steamify: check for a new release daily - -[Timer] -OnCalendar=daily -RandomizedDelaySec=1h -Persistent=true - -[Install] -WantedBy=timers.target -EOF + service_file "$NOTIFY_NAME.service" NOTIFY_SCRIPT="$NOTIFY_SCRIPT" > "$NOTIFY_UNITS/$NOTIFY_NAME.service" + service_file "$NOTIFY_NAME.timer" > "$NOTIFY_UNITS/$NOTIFY_NAME.timer" systemctl --user daemon-reload systemctl --user enable "$NOTIFY_NAME.service" >/dev/null 2>&1 && systemctl --user enable --now "$NOTIFY_NAME.timer" >/dev/null 2>&1 || diff --git a/services/README.md b/services/README.md new file mode 100644 index 0000000..110e98a --- /dev/null +++ b/services/README.md @@ -0,0 +1,16 @@ +# Services + +The systemd units Steamify installs, kept out of the shell code so they can +be read and reviewed on their own. `lib/*.sh` reads them with `service_file + [KEY=value...]`, which fills in `@KEY@` placeholders (paths the +scripts define); the single-file build (`.github/tools/bundle.sh`) embeds +them. Small drop-ins that only add a line or two to someone else's unit stay +in the shell code. + +| File | Used by | What | +|---|---|---| +| `steamify-boot-desktop.service` | `lib/boot-session.sh` (Boot into: desktop) | Sets the Plasma session before the login manager starts | +| `sync-steamos-session.path`, `.service` | `lib/login-manager.sh` (SteamOS conversion, plasma-login-manager) | The sync bridge: copies Steam's session choice into `plasmalogin.conf` | +| `steam-desktop-autostart.service` | `lib/steam-desktop.sh` (SteamOS conversion) | Steam in the background on the Plasma desktop (user unit) | +| `ensure-kernel-headers.service` | `lib/steam-machine.sh` (Steam Machine support) | Installs missing kernel headers at boot, so DKMS builds for every kernel | +| `steamify-update-check.service`, `.timer` | `lib/update-notifier.sh` (Update notifications) | The daily and login check for a new release (user units) | diff --git a/services/ensure-kernel-headers.service b/services/ensure-kernel-headers.service new file mode 100644 index 0000000..095886a --- /dev/null +++ b/services/ensure-kernel-headers.service @@ -0,0 +1,11 @@ +[Unit] +Description=Install missing kernel headers so DKMS builds leds-valve for every kernel +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=@HEADERS_SCRIPT@ + +[Install] +WantedBy=multi-user.target diff --git a/services/steam-desktop-autostart.service b/services/steam-desktop-autostart.service new file mode 100644 index 0000000..b1c3323 --- /dev/null +++ b/services/steam-desktop-autostart.service @@ -0,0 +1,15 @@ +[Unit] +Description=Steam Background Autostart for Virtual Keyboard +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +# Only in the Plasma desktop session; gamescope-session starts its own Steam. +ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]' +ExecStart=/usr/bin/steam -silent +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=graphical-session.target diff --git a/services/steamify-boot-desktop.service b/services/steamify-boot-desktop.service new file mode 100644 index 0000000..8be2852 --- /dev/null +++ b/services/steamify-boot-desktop.service @@ -0,0 +1,11 @@ +[Unit] +Description=Steamify CachyOS: boot into the desktop instead of gaming mode +Before=display-manager.service sddm.service plasmalogin.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/steamos/steam-set-session plasma.desktop +ExecStart=/bin/sh -c '[ -x @SESSION_SYNC@ ] && exec @SESSION_SYNC@ || true' + +[Install] +WantedBy=graphical.target diff --git a/services/steamify-update-check.service b/services/steamify-update-check.service new file mode 100644 index 0000000..8265476 --- /dev/null +++ b/services/steamify-update-check.service @@ -0,0 +1,14 @@ +[Unit] +Description=Steamify: check for a new release +After=plasma-workspace.target +# The tray icon goes with a logout. +PartOf=graphical-session.target + +[Service] +Type=simple +# The short wait at login lets the tray come up first (in the script: an +# ExecStartPre sleep runs into the start timeout). +ExecStart=@NOTIFY_SCRIPT@ --delay 60 + +[Install] +WantedBy=plasma-workspace.target diff --git a/services/steamify-update-check.timer b/services/steamify-update-check.timer new file mode 100644 index 0000000..390f5e5 --- /dev/null +++ b/services/steamify-update-check.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Steamify: check for a new release daily + +[Timer] +OnCalendar=daily +RandomizedDelaySec=1h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/services/sync-steamos-session.path b/services/sync-steamos-session.path new file mode 100644 index 0000000..d1cc221 --- /dev/null +++ b/services/sync-steamos-session.path @@ -0,0 +1,8 @@ +[Unit] +Description=Watch for steamos session changes + +[Path] +PathModified=/etc/plasmalogin.conf.d/zz-steamos-autologin.conf + +[Install] +WantedBy=multi-user.target diff --git a/services/sync-steamos-session.service b/services/sync-steamos-session.service new file mode 100644 index 0000000..d0ab93d --- /dev/null +++ b/services/sync-steamos-session.service @@ -0,0 +1,9 @@ +[Unit] +Description=Sync steamos session selection into plasmalogin.conf +# Session switches can come in bursts (switch + autologin reset); never +# let systemd's start rate limit silently stop the bridge. +StartLimitIntervalSec=0 + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/sync-steamos-session.sh diff --git a/ui/steamify-app.sh b/ui/steamify-app.sh index 35a267b..c77f21c 100755 --- a/ui/steamify-app.sh +++ b/ui/steamify-app.sh @@ -2,7 +2,7 @@ # Steamify CachyOS - get and start the app (v2), for `curl | bash`: # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash # Downloads the newest release's app package (steamify-app.tar.gz: the app, -# steamify.sh, lib/ and patches/) to ~/.local/share/steamify/app, installs PySide6 if +# steamify.sh, lib/, patches/ and services/) to ~/.local/share/steamify/app, installs PySide6 if # it's missing, adds a launcher entry, and starts the app. # STEAMIFY_RELEASE= takes that release instead of the newest (the # v2-ui-preview build's copy of this script sets it). STEAMIFY_BRANCH=