From cf73229756eee3bf1db0bc58dcf46220cb98efad Mon Sep 17 00:00:00 2001 From: rickpeters Date: Sun, 27 Sep 2026 14:06:48 +0200 Subject: [PATCH] feat: Update notifications: a notification and tray icon when there's a new Steamify --- AGENTS.md | 7 ++ CHANGELOG.md | 4 ++ README.md | 8 ++- TECHNICAL.md | 22 ++++++ lib/menu.sh | 5 +- lib/update-notifier.sh | 82 +++++++++++++++++++++ patches/README.md | 1 + patches/steamify-notifier.py | 133 +++++++++++++++++++++++++++++++++++ steamify.sh | 5 +- ui/qml/Texts.qml | 3 + 10 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 lib/update-notifier.sh create mode 100644 patches/steamify-notifier.py diff --git a/AGENTS.md b/AGENTS.md index 7ea1e15..56900cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -223,6 +223,13 @@ 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. +- 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 + compares GitHub's newest release with the `seen` version every run records + (`notify_seen` in the entry point, only while it's on). It must never + update anything itself: notification + tray icon, Open Steamify / Skip + this version (`skipped`). Desktop only (exits without `plasmashell`). - VRAM booster (`vram`, `lib/vram-booster.sh`, top-level, ticked by default, 2.3.0; only available when `/sys/fs/cgroup/dmem.capacity` lists a VRAM region of at least 2 GB; greyed out with an NVIDIA card, diff --git a/CHANGELOG.md b/CHANGELOG.md index aa246f0..cbd3570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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.0 - 2026-09-27 + +- **feat: Update notifications: a notification and tray icon when there's a new Steamify** + ## 2.4.1 - 2026-09-27 - **chore: Version 2.4.1** diff --git a/README.md b/README.md index 8957611..20c66da 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,16 @@ 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. -6. **VRAM booster** - on by default: the game you're playing keeps its video +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 + ([details](TECHNICAL.md#update-notifications)). +7. **VRAM booster** - on by default: the game you're playing keeps its video memory and background apps make room, like SteamOS 3.9. Shown for GPUs whose driver supports it (AMD, Intel) on kernel 7.2 or newer; greyed out with an NVIDIA card until NVIDIA's driver supports it ([details](TECHNICAL.md#vram-booster)). -7. **HDMI-CEC** (experimental; off by default, on by default on a Steam +8. **HDMI-CEC** (experimental; off by default, on by default on a Steam Machine) - use Steam with your TV's remote, and the TV turns on and off with the PC. Needs a PC with CEC (like the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA diff --git a/TECHNICAL.md b/TECHNICAL.md index 9bbd3bc..585bbc6 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -236,6 +236,27 @@ 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. +## Update notifications + +The **Update notifications** option (new in 2.5.0, ticked by default) +installs `patches/steamify-notifier.py` as +`~/.local/share/cachyos-gamescope-boot/steamify-notifier` with two user +units in `~/.config/systemd/user`: `steamify-update-check.timer` (daily, +`Persistent=`) and `steamify-update-check.service` (also wanted by +`plasma-workspace.target`, so it checks a minute after each desktop login). +Nothing stays running while there's no update. + +Every Steamify run records its version as `seen` (`notify.state`). The check +asks GitHub for the newest release; when it's newer than `seen` and not the +`skipped` version, it shows a notification (`notify-send` with the buttons +**Open Steamify** and **Skip this version**) and a tray icon (click: open; +menu: open, skip, remind me later). Closing the notification keeps the +icon; opening Steamify in any way records the new version and ends it. It +only runs on the Plasma desktop: gamescope shows no notifications or tray, +so an update found in gaming mode waits for the next desktop login. It never +installs anything: the shortcut always starts the newest release, which then +shows what's new or updated for this install. + ## VRAM booster SteamOS 3.9 manages the dGPU's VRAM per cgroup: the game in front is @@ -357,6 +378,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/update-notifier.sh` | Update notifications: the notifier from `patches/` and its user timer | | `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) | | `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 | diff --git a/lib/menu.sh b/lib/menu.sh index 98c91c2..20832b7 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -5,7 +5,7 @@ # 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 vram cec machine poweroff kpin hdmi bios) +COMPONENTS=(gaming boot theme glyphs single launcher 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) @@ -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 + [launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [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" + [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)" [machine]="Steam Machine support: LED bar driver, hardware settings in Steam" diff --git a/lib/update-notifier.sh b/lib/update-notifier.sh new file mode 100644 index 0000000..2b3c7d4 --- /dev/null +++ b/lib/update-notifier.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# "Update notifications" menu item (notify), ticked by default: a systemd +# user timer checks GitHub's newest release once a day and at each Plasma +# login. When it's newer than the last Steamify that ran here, it shows a +# notification (Open Steamify / Skip this version) and a tray icon until +# then; it never updates anything itself (patches/steamify-notifier.py). +# Sourced by steamify.sh; not meant to be run on its own. + +NOTIFY_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot" +NOTIFY_SCRIPT="$NOTIFY_DIR/steamify-notifier" +NOTIFY_UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" +NOTIFY_NAME=steamify-update-check + +notify_status() { + [[ -f "$NOTIFY_SCRIPT" ]] && + systemctl --user is-enabled -q "$NOTIFY_NAME.timer" 2>/dev/null && + systemctl --user is-enabled -q "$NOTIFY_NAME.service" 2>/dev/null +} + +notify_seen() { + # Every run records its version: the notifier compares the newest + # release with it, so opening Steamify ends a reminder. + notify_status && state_set notify seen "$VERSION" +} + +notify_enable() { + local p missing=() + info "Adding update notifications..." + # PySide6 for the tray icon, notify-send for the notification's buttons. + python3 -c 'import PySide6.QtWidgets' 2>/dev/null || missing+=(pyside6) + command -v notify-send >/dev/null || missing+=(libnotify) + if ((${#missing[@]})); then + sudo pacman -S --needed --noconfirm "${missing[@]}" || + { err "Installing ${missing[*]} failed."; return 1; } + fi + 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 + 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 || + { err "Enabling the update check failed."; return 1; } + state_set notify seen "$VERSION" + ok "Update notifications on: you'll get a notification when there's a new Steamify." +} + +notify_disable() { + systemctl --user disable --now "$NOTIFY_NAME.timer" "$NOTIFY_NAME.service" >/dev/null 2>&1 + rm -f "$NOTIFY_UNITS/$NOTIFY_NAME.service" "$NOTIFY_UNITS/$NOTIFY_NAME.timer" "$NOTIFY_SCRIPT" + rmdir "$NOTIFY_DIR" 2>/dev/null || true + systemctl --user daemon-reload + state_clear notify + ok "Update notifications off." +} diff --git a/patches/README.md b/patches/README.md index 23ee437..edd8556 100644 --- a/patches/README.md +++ b/patches/README.md @@ -7,4 +7,5 @@ read and reviewed on their own. `lib/*.sh` reads them with `patch_file | File | Used by | What | |---|---|---| | `steamify-fremont-poweroff.c` | `lib/fremont-poweroff.sh` (Steam Machine support) | Kernel module (DKMS): clears GPIO 18's S4/S5 wake bit before power-off, so the Steam Machine stays off with recent kernels | +| `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/steamify-notifier.py b/patches/steamify-notifier.py new file mode 100644 index 0000000..3daa40e --- /dev/null +++ b/patches/steamify-notifier.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# Steamify CachyOS: update notifications (installed by the "Update +# notifications" menu item, run by a systemd user timer and at each Plasma +# login). Never updates anything itself: when GitHub has a newer release +# than the last Steamify that ran here, it shows a notification (Open +# Steamify / Skip this version) and a tray icon until you open the app, +# skip the version or pick "Remind me later". Nothing runs in between. +import configparser +import json +import os +import subprocess +import sys +import threading +import time +import urllib.request + +REPO = "theupriser/steamify-cachyos" +STATE_DIR = os.path.join(os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state"), + "cachyos-gamescope-boot") +STATE = os.path.join(STATE_DIR, "notify.state") +DATA = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share") +RUN_APP = os.path.join(DATA, "cachyos-gamescope-boot", "run-app") +APP_URL = f"https://github.com/{REPO}/releases/latest/download/steamify-app.sh" + + +def version(v): + try: + return tuple(int(p) for p in v.lstrip("v").split(".")) + except ValueError: + return None + + +def state(key): + cp = configparser.ConfigParser(interpolation=None) + cp.read(STATE) + return cp.get("State", key, fallback="") + + +def set_state(key, value): + # Same ini as lib/state.sh's state_set writes. + subprocess.run(["kwriteconfig6", "--file", STATE, "--group", "State", "--key", key, value], check=False) + + +def latest(): + req = urllib.request.Request(f"https://api.github.com/repos/{REPO}/releases/latest", + headers={"User-Agent": "steamify-notifier", "Accept": "application/vnd.github+json"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r)["tag_name"].lstrip("v") + + +def pending(new): + # Newer than the last Steamify that ran here, and not skipped. + seen = version(state("seen") or "0") + return seen is not None and version(new) > seen and new != state("skipped") + + +def desktop(): + # Only on the Plasma desktop: gamescope doesn't show notifications or a + # tray, so an update found in gaming mode waits for the next desktop login. + return subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "plasmashell"], + stdout=subprocess.DEVNULL).returncode == 0 + + +def main(): + if "--delay" in sys.argv: + time.sleep(int(sys.argv[sys.argv.index("--delay") + 1])) + if not desktop(): + return 0 + try: + new = latest() + except Exception as e: # offline, rate limited: try again next time + print(f"Couldn't check for a new Steamify: {e}", file=sys.stderr) + return 0 + if version(new) is None or not pending(new): + return 0 + + from PySide6.QtCore import QObject, QTimer, Signal + from PySide6.QtGui import QAction, QIcon + from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon + + app = QApplication(sys.argv[:1]) + app.setApplicationName("Steamify") + app.setDesktopFileName("steamify-ui") + app.setQuitOnLastWindowClosed(False) + icon = QIcon.fromTheme("cachyos-gamescope-boot-wizard", QIcon.fromTheme("steam")) + + def open_app(): + # The app records its version as seen, which ends the reminder. + cmd = [RUN_APP] if os.access(RUN_APP, os.X_OK) else \ + ["bash", "-c", f"set -o pipefail; curl -fsSL --max-time 30 {APP_URL} | bash"] + subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + app.quit() + + def skip(): + set_state("skipped", new) + app.quit() + + tray = QSystemTrayIcon(icon) + tray.setToolTip(f"Steamify {new} is available") + menu = QMenu() + for text, fn in ((f"Open Steamify {new}", open_app), ("Skip this version", skip), ("Remind me later", app.quit)): + a = QAction(text, menu) + a.triggered.connect(fn) + menu.addAction(a) + tray.setContextMenu(menu) + tray.activated.connect(lambda reason: open_app() if reason == QSystemTrayIcon.ActivationReason.Trigger else None) + tray.show() + + # The notification's buttons: notify-send waits for the click, so it + # runs beside the event loop. Closing it without a click keeps the icon. + class Relay(QObject): + picked = Signal(str) + relay = Relay() + relay.picked.connect(lambda a: open_app() if a == "open" else skip() if a == "skip" else None) + + def notify(): + r = subprocess.run(["notify-send", "--app-name=Steamify", "--icon=cachyos-gamescope-boot-wizard", + "-A", "open=Open Steamify", "-A", "skip=Skip this version", + f"Steamify {new} is available", + "Open Steamify to see what's new and choose what to update."], + capture_output=True, text=True) + relay.picked.emit(r.stdout.strip()) + threading.Thread(target=notify, daemon=True).start() + + # Opened some other way (the shortcut, the terminal): the reminder is done. + poll = QTimer() + poll.timeout.connect(lambda: None if pending(new) else app.quit()) + poll.start(60_000) + return app.exec() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/steamify.sh b/steamify.sh index c830f32..89ea74b 100755 --- a/steamify.sh +++ b/steamify.sh @@ -15,11 +15,11 @@ set -uo pipefail # Release version, see CHANGELOG.md. -VERSION=2.4.1 +VERSION=2.5.0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do +for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut update-notifier menu backend; do # shellcheck source=/dev/null source "$SCRIPT_DIR/lib/$lib.sh" done @@ -43,6 +43,7 @@ fi # Everything per-user (autologin user, Steam, theme, shortcut) is for the # user running the script. TARGET_USER="$(id -un)" +notify_seen restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; } diff --git a/ui/qml/Texts.qml b/ui/qml/Texts.qml index 807bd9a..6c3f3b8 100644 --- a/ui/qml/Texts.qml +++ b/ui/qml/Texts.qml @@ -21,6 +21,9 @@ QtObject { launcher: { label: "Steamify shortcut", hint: "The app on the desktop and in the launcher", body: "Opens the newest Steamify app, so you never need the install command again. Steamify Terminal in the launcher opens the terminal menu.", changes: ["Steamify CachyOS on the desktop and in the launcher (the app)", "Steamify Terminal in the launcher"] }, + notify: { label: "Update notifications", hint: "A notification when there's a new Steamify", + body: "Checks once a day and when you log in to the desktop. When there's a new Steamify you get a notification and a tray icon: open the app to see what's new and choose what to update, or skip that version. It never updates anything by itself.", + changes: ["A daily check (systemd user timer), only on the desktop", "Tray icon only while an update is waiting", "Skip this version: quiet until the next one"] }, vram: { label: "VRAM booster", hint: "The game in front keeps its VRAM", body: "Like SteamOS 3.9: the game you're playing keeps its video memory, and background apps are moved out first. Without it a game that needs most of the video memory can spill into system RAM and stutter. Shown for GPUs whose driver supports it (AMD, Intel) on kernel 7.2 or newer.", changes: ["dmemcg-booster (system and user service)", "plasma-foreground-booster, which tells it which window is in front", "Off: removed again, unless you had installed them yourself"] },