mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
Compare commits
159
Commits
v2.3.0
..
1074514b83
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1074514b83 | ||
|
|
2c004322af | ||
|
|
3915e1a96f | ||
|
|
098dc3d0d8 | ||
|
|
48352b5601 | ||
|
|
05b8f11c19 | ||
|
|
8b96e2d8a9 | ||
|
|
f1fb18ec81 | ||
|
|
1987fa903b | ||
|
|
b7dc931a4f | ||
|
|
3d20a7edb0 | ||
|
|
f1983a9caf | ||
|
|
01a77b7dc7 | ||
|
|
83248a8478 | ||
|
|
5811f5c8a4 | ||
|
|
71bb2e7aca | ||
|
|
d0e4d8cf79 | ||
|
|
d1373e28a6 | ||
|
|
d6db4f6f2d | ||
|
|
a5e9015d93 | ||
|
|
ae238ff383 | ||
|
|
c1217bcea3 | ||
|
|
f3d67fbe52 | ||
|
|
84cf0959e4 | ||
|
|
7caefb7da6 | ||
|
|
453656a0a2 | ||
|
|
6aa1377e16 | ||
|
|
6bcae8af18 | ||
|
|
3cbeb58211 | ||
|
|
83561bb214 | ||
|
|
ca027cb607 | ||
|
|
d93f9ea56c | ||
|
|
761f921e40 | ||
|
|
7a54a2fa59 | ||
|
|
9eab3978a9 | ||
|
|
9602d9d00b | ||
|
|
9a2965d339 | ||
|
|
c42d5454bf | ||
|
|
b3bed43854 | ||
|
|
3e79451244 | ||
|
|
971e792054 | ||
|
|
6557320c81 | ||
|
|
692e3fcbad | ||
|
|
70ecb563fd | ||
|
|
a7276cfd9a | ||
|
|
b3549d4eea | ||
|
|
a232a55bef | ||
|
|
397b3cb607 | ||
|
|
bda1d3ebbe | ||
|
|
3676c64c90 | ||
|
|
d44f29956a | ||
|
|
2858d13429 | ||
|
|
48cf3c7c7c | ||
|
|
619ef1f714 | ||
|
|
b6240539e6 | ||
|
|
7d6df21627 | ||
|
|
3cad70dcc4 | ||
|
|
b8742a5642 | ||
|
|
7f5d29c997 | ||
|
|
cfe3ce7bd0 | ||
|
|
3e4133c14a | ||
|
|
c4b8db85dc | ||
|
|
350011877b | ||
|
|
b1d79d48a6 | ||
|
|
5b42036786 | ||
|
|
4a64232fad | ||
|
|
4ec92f4d89 | ||
|
|
fe4f3f43be | ||
|
|
4bacc3b2e6 | ||
|
|
cc0ca1c35e | ||
|
|
580cc71b57 | ||
|
|
c87e6c763f | ||
|
|
fc05725a91 | ||
|
|
90bc5e4408 | ||
|
|
d99ff652dc | ||
|
|
2aa3068b60 | ||
|
|
1a6ddcfc0d | ||
|
|
11668db91c | ||
|
|
3316c1de28 | ||
|
|
d9365dac81 | ||
|
|
853a3926ee | ||
|
|
66ea63cf7e | ||
|
|
f560f332a9 | ||
|
|
9b86cd1a6c | ||
|
|
a72a4def71 | ||
|
|
ea051c77f6 | ||
|
|
4e829c6111 | ||
|
|
d73e71d4eb | ||
|
|
8f2d2be2aa | ||
|
|
53b72c8e33 | ||
|
|
3b4f3b2313 | ||
|
|
a72d848e78 | ||
|
|
3a58c72d16 | ||
|
|
27a40702ce | ||
|
|
8ab066f034 | ||
|
|
44bfe071d6 | ||
|
|
e281d5e879 | ||
|
|
435f69c151 | ||
|
|
9319cbca1e | ||
|
|
cf6bee1c86 | ||
|
|
16487ba00c | ||
|
|
07a94cd274 | ||
|
|
ce71b34b0d | ||
|
|
d30d505f7d | ||
|
|
9755cc3ac3 | ||
|
|
6f26e74229 | ||
|
|
2a01583b8f | ||
|
|
c40aa1f7d9 | ||
|
|
d204efb59d | ||
|
|
12f6cc0b3a | ||
|
|
b01e2c5221 | ||
|
|
affb5f79b0 | ||
|
|
58e43d4c4a | ||
|
|
23f265b02f | ||
|
|
ced4a45d2f | ||
|
|
6ad686a8b2 | ||
|
|
b1adacbcb3 | ||
|
|
8dd2db3a97 | ||
|
|
501c0ba425 | ||
|
|
bee18b76a8 | ||
|
|
eadbdda271 | ||
|
|
c4ec755a4a | ||
|
|
14aa7c0b54 | ||
|
|
5bf9da411d | ||
|
|
4f6e6fd0aa | ||
|
|
a79eeedbc4 | ||
|
|
b948c2020b | ||
|
|
0c96bed448 | ||
|
|
e3f7cc9773 | ||
|
|
353de489d7 | ||
|
|
d8b2e812c5 | ||
|
|
3a0692fdf2 | ||
|
|
e86ddcc4af | ||
|
|
389bc3346e | ||
|
|
c6c1150836 | ||
|
|
98e2effcff | ||
|
|
431515b60f | ||
|
|
34c9cfdb32 | ||
|
|
8ddb7ebf3d | ||
|
|
32c33e4abf | ||
|
|
2dd0f3ac47 | ||
|
|
57995bcd1f | ||
|
|
d57e8d8448 | ||
|
|
5a34633986 | ||
|
|
479b70b337 | ||
|
|
2e0881b4fa | ||
|
|
cce31e19c2 | ||
|
|
bd4fa3a6f4 | ||
|
|
8ffa0f2449 | ||
|
|
8a2601d73d | ||
|
|
572417bd7b | ||
|
|
cf73229756 | ||
|
|
febdab5b53 | ||
|
|
90cdfd6666 | ||
|
|
6f598d18fd | ||
|
|
9bbe5fa602 | ||
|
|
ceeb949e6b | ||
|
|
03a8b8c4ba | ||
|
|
e699a3f8eb |
No files matched your search
+11
-6
@@ -31,13 +31,15 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty
|
|||||||
sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh"
|
sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh"
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
# patches/ embedded: replaces lib/common.sh's patch_file, which reads
|
# patches/ and services/ embedded: replace lib/common.sh's patch_file
|
||||||
# them from the checkout.
|
# and service_raw, which read them from the checkout.
|
||||||
echo "# ----- patches/ -----"
|
embed() {
|
||||||
echo "patch_file() {"
|
local fn="$1" dir="$2" f
|
||||||
|
echo "# ----- $dir/ -----"
|
||||||
|
echo "$fn() {"
|
||||||
echo ' case "$1" in'
|
echo ' case "$1" in'
|
||||||
for f in patches/*; do
|
for f in "$dir"/*; do
|
||||||
[[ "$f" == patches/README.md ]] && continue
|
[[ "$f" == "$dir/README.md" ]] && continue
|
||||||
grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; }
|
grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; }
|
||||||
echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'"
|
echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'"
|
||||||
cat "$f"
|
cat "$f"
|
||||||
@@ -48,6 +50,9 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty
|
|||||||
echo ' esac'
|
echo ' esac'
|
||||||
echo "}"
|
echo "}"
|
||||||
echo
|
echo
|
||||||
|
}
|
||||||
|
embed patch_file patches
|
||||||
|
embed service_raw services
|
||||||
echo "# ----- steamify.sh -----"
|
echo "# ----- steamify.sh -----"
|
||||||
echo "main() {"
|
echo "main() {"
|
||||||
# Everything after the source loop is the entry point's body.
|
# Everything after the source loop is the entry point's body.
|
||||||
|
|||||||
+114
-25
@@ -5,18 +5,16 @@
|
|||||||
# release are moved to it, so both of these always get the newest version:
|
# release are moved to it, so both of these always get the newest version:
|
||||||
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
|
# 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
|
# 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,
|
# Each release also has the app: 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
|
# 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
|
# Pushes to release/** build and check it like pull requests; only main publishes.
|
||||||
# v2-ui-preview instead, whose launcher gets that build:
|
|
||||||
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/v2-ui-preview/steamify-app.sh | bash
|
|
||||||
# Pull requests only build and check it, and warn when VERSION is already released.
|
# Pull requests only build and check it, and warn when VERSION is already released.
|
||||||
name: Single-file build
|
name: Single-file build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, feat/v2-ui]
|
branches: [main, 'release/**']
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -36,18 +34,45 @@ jobs:
|
|||||||
.github/tools/bundle.sh dist/steamify.sh
|
.github/tools/bundle.sh dist/steamify.sh
|
||||||
# The app: sources, not the single-file build (its backend mode runs
|
# The app: sources, not the single-file build (its backend mode runs
|
||||||
# steamify.sh from its own folder).
|
# 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
|
cp ui/steamify-app.sh dist/steamify-app.sh
|
||||||
|
|
||||||
- name: Check
|
- name: Check
|
||||||
run: |
|
run: |
|
||||||
bash -n dist/steamify.sh
|
bash -n dist/steamify.sh
|
||||||
# SC2034/SC2154: variables used across the inlined modules.
|
|
||||||
shellcheck -S warning -e SC2034,SC2154 dist/steamify.sh
|
|
||||||
shellcheck -S warning dist/steamify-app.sh
|
|
||||||
python3 -m py_compile ui/steamify-ui
|
python3 -m py_compile ui/steamify-ui
|
||||||
|
|
||||||
|
# shellcheck through an action that brings its own binary: GitHub's runner has it installed,
|
||||||
|
# Gitea's act_runner image doesn't. One file per run, as before (the exceptions are the bundle's).
|
||||||
|
- name: shellcheck the bundle
|
||||||
|
uses: ludeeus/action-shellcheck@2.0.0
|
||||||
|
env:
|
||||||
|
# SC2034/SC2154: variables used across the inlined modules.
|
||||||
|
SHELLCHECK_OPTS: -e SC2034 -e SC2154
|
||||||
|
with:
|
||||||
|
severity: warning
|
||||||
|
scandir: dist
|
||||||
|
ignore_names: steamify-app.sh
|
||||||
|
- name: shellcheck the app launcher
|
||||||
|
uses: ludeeus/action-shellcheck@2.0.0
|
||||||
|
with:
|
||||||
|
severity: warning
|
||||||
|
scandir: dist
|
||||||
|
ignore_names: steamify.sh
|
||||||
|
|
||||||
|
# The build as an artifact on GitHub and on the mirror (Gitea); releases stay GitHub's.
|
||||||
|
# Gitea's artifact server doesn't speak upload-artifact@v4's protocol, and GitHub fails a workflow
|
||||||
|
# that merely mentions the deprecated @v3: Gitea gets christopherhx's fork of v4 for Gitea.
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
|
with:
|
||||||
|
name: steamify
|
||||||
|
path: |
|
||||||
|
dist/steamify.sh
|
||||||
|
dist/steamify-app.tar.gz
|
||||||
|
dist/steamify-app.sh
|
||||||
|
- uses: christopherhx/gitea-upload-artifact@v4
|
||||||
|
if: github.server_url != 'https://github.com'
|
||||||
with:
|
with:
|
||||||
name: steamify
|
name: steamify
|
||||||
path: |
|
path: |
|
||||||
@@ -57,6 +82,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Version
|
- name: Version
|
||||||
id: version
|
id: version
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
@@ -71,6 +97,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish the v${{ steps.version.outputs.version }} release
|
- name: Publish the v${{ steps.version.outputs.version }} release
|
||||||
if: >-
|
if: >-
|
||||||
|
github.server_url == 'https://github.com' &&
|
||||||
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
|
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
|
||||||
steps.version.outputs.released == 'false'
|
steps.version.outputs.released == 'false'
|
||||||
env:
|
env:
|
||||||
@@ -107,22 +134,84 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
gh release upload latest dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg --clobber
|
gh release upload latest dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg --clobber
|
||||||
|
|
||||||
- name: Publish the v2-ui-preview pre-release
|
# The mirror (Gitea) publishes its own release per version too, the same way: never overwrites one,
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/feat/v2-ui'
|
# notes from the changelog. gh can't talk to Gitea: its API for the check, a Gitea release action to
|
||||||
|
# publish. The token is the run's own (repository write); a secret RELEASE_TOKEN overrides it.
|
||||||
|
- name: Version (mirror)
|
||||||
|
id: mirror_version
|
||||||
|
if: github.server_url != 'https://github.com'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
|
||||||
run: |
|
run: |
|
||||||
# Its launcher downloads this pre-release, not the newest release.
|
version="$(grep -oP '^VERSION=\K.*' steamify.sh)"
|
||||||
sed -i 's/STEAMIFY_RELEASE:-latest/STEAMIFY_RELEASE:-v2-ui-preview/' dist/steamify-app.sh
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
grep -q 'STEAMIFY_RELEASE:-v2-ui-preview' dist/steamify-app.sh
|
if curl -fsS -o /dev/null -H "Authorization: token $TOKEN" \
|
||||||
git tag -f v2-ui-preview "$GITHUB_SHA" && git push -f origin v2-ui-preview
|
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/v$version"; then
|
||||||
notes="Preview of the Steamify app (v2) from feat/v2-ui ($(git describe --always), $(git log -1 --format='%cs %s')). Not a release.
|
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||||
Run it with:
|
echo "::warning::v$version is already released on this server and won't be overwritten."
|
||||||
|
|
||||||
curl -fsSL https://github.com/${{ github.repository }}/releases/download/v2-ui-preview/steamify-app.sh | bash"
|
|
||||||
if gh release view v2-ui-preview >/dev/null 2>&1; then
|
|
||||||
gh release edit v2-ui-preview --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
|
|
||||||
else
|
else
|
||||||
gh release create v2-ui-preview --target "$GITHUB_SHA" --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
|
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
gh release upload v2-ui-preview dist/steamify-app.sh dist/steamify-app.tar.gz --clobber
|
|
||||||
|
- name: Release notes (mirror)
|
||||||
|
if: >-
|
||||||
|
github.server_url != 'https://github.com' &&
|
||||||
|
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
|
||||||
|
steps.mirror_version.outputs.released == 'false'
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.mirror_version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
changes="$(awk -v v="$VERSION" '
|
||||||
|
$0 ~ "^## " { if (found) exit; if (index($0, "## " v " ") == 1) { found = 1; next } }
|
||||||
|
found' CHANGELOG.md)"
|
||||||
|
cat > dist/release-notes.md <<NOTES
|
||||||
|
Single-file build of v$VERSION ($(git describe --always), $(git log -1 --format=%cs)).
|
||||||
|
Run this version with:
|
||||||
|
|
||||||
|
curl -fsSL ${{ github.server_url }}/${{ github.repository }}/releases/download/v$VERSION/steamify.sh | bash
|
||||||
|
|
||||||
|
## Changes in v$VERSION
|
||||||
|
$changes
|
||||||
|
NOTES
|
||||||
|
|
||||||
|
- name: Publish the v${{ steps.mirror_version.outputs.version }} release (mirror)
|
||||||
|
if: >-
|
||||||
|
github.server_url != 'https://github.com' &&
|
||||||
|
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
|
||||||
|
steps.mirror_version.outputs.released == 'false'
|
||||||
|
uses: akkuman/gitea-release-action@v1.3.7
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN || github.token }}
|
||||||
|
tag_name: v${{ steps.mirror_version.outputs.version }}
|
||||||
|
target_commitish: ${{ github.sha }}
|
||||||
|
name: Steamify CachyOS v${{ steps.mirror_version.outputs.version }}
|
||||||
|
body_path: dist/release-notes.md
|
||||||
|
files: |
|
||||||
|
dist/steamify.sh
|
||||||
|
dist/steamify-app.sh
|
||||||
|
dist/steamify-app.tar.gz
|
||||||
|
assets/steam-gaming-settings.svg
|
||||||
|
|
||||||
|
# A new version starts the Steamify ISO's release of the ISO repo: its iso-1-github-tag.yml (always run from
|
||||||
|
# master) names the ISO after the newest Steamify release, tags it, and the Gitea mirror builds and attaches the ISO.
|
||||||
|
# The branch here decides the kind of build (the workflow's input `kind`):
|
||||||
|
# main (publishes the release) -> release: a real ISO (tag v<steamify>-<date>)
|
||||||
|
# release/** (a version not yet released) -> dev: a pre-release (tag v<steamify>-dev.<date>)
|
||||||
|
# Needs a fine-grained token for that repo (Actions: read and write) as the secret ISO_DISPATCH_TOKEN;
|
||||||
|
# without it, this is skipped.
|
||||||
|
- name: Start the Steamify ISO's release
|
||||||
|
if: >-
|
||||||
|
github.server_url == 'https://github.com' &&
|
||||||
|
github.event_name != 'pull_request' &&
|
||||||
|
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) &&
|
||||||
|
steps.version.outputs.released == 'false'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.ISO_DISPATCH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$GH_TOKEN" ]; then
|
||||||
|
echo "::notice::No ISO_DISPATCH_TOKEN: the Steamify ISO isn't released with this version."; exit 0
|
||||||
|
fi
|
||||||
|
# A release branch builds the ISO with its own code (and VERSION): steamify_ref. main builds the release it just published.
|
||||||
|
if [ "$GITHUB_REF" = refs/heads/main ]; then kind=release; sref=""; else kind=dev; sref="$GITHUB_REF_NAME"; fi
|
||||||
|
gh workflow run iso-1-github-tag.yml -R theupriser/steamify-cachyos-live-iso --ref master -f kind="$kind" -f steamify_ref="$sref"
|
||||||
|
echo "Started the Steamify ISO's release (theupriser/steamify-cachyos-live-iso, master, kind $kind, Steamify ${sref:-the newest release})"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Sync git.upriser.nl mirror
|
||||||
|
|
||||||
|
# Trigger the Gitea pull-mirror immediately instead of waiting for its interval,
|
||||||
|
# so Gitea builds/tests right after any branch or tag lands on GitHub (the mirror has no sync interval of its own).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
tags: ['*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
# Gitea also reads .github/workflows on the mirror; only run on GitHub
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger mirror-sync
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ secrets.GIT_UPRISER_URL }}
|
||||||
|
GIT_UPRISER_TOKEN: ${{ secrets.GIT_UPRISER_TOKEN }}
|
||||||
|
# Gitea "owner/repo"; defaults to the same path as on GitHub
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.repository }}
|
||||||
|
run: |
|
||||||
|
curl --fail-with-body -sS --retry 5 --retry-delay 10 --retry-all-errors -X POST \
|
||||||
|
-H "Authorization: token ${GIT_UPRISER_TOKEN}" \
|
||||||
|
"${GITEA_URL%/}/api/v1/repos/${GITEA_REPO}/mirror-sync"
|
||||||
@@ -19,6 +19,25 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
apply, never inline in the shell code. Read them with `patch_file
|
apply, never inline in the shell code. Read them with `patch_file
|
||||||
<name>` (`lib/common.sh`, from the checkout); the bundle embeds every file
|
<name>` (`lib/common.sh`, from the checkout); the bundle embeds every file
|
||||||
in `patches/` and overrides `patch_file`. Add new ones to its README.
|
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 <name> [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
|
||||||
|
`*Screen.qml` per screen, small widgets (`Btn`, `Chip`, `Badge`, ...), and
|
||||||
|
the singletons `Theme` (colours, fonts), `Texts` (item texts) and `Input`
|
||||||
|
(controller/keyboard/remote and its button names), listed in `qmldir`.
|
||||||
|
Screens get the `AppState` as `app` and only show it or call its functions.
|
||||||
|
- **Paths.** Everything in the home folder lives under `steamify`:
|
||||||
|
`$STATE_DIR` (`~/.local/state/steamify`), `$STEAMIFY_DATA`/`$STEAMIFY_BIN`
|
||||||
|
(`~/.local/share/steamify/{app,bin}`), `lib/state.sh`. Never add files
|
||||||
|
under the old name `cachyos-gamescope-boot`; `migrate_layout` moves those
|
||||||
|
of older versions (symlinks keep older releases working). Keep the
|
||||||
|
`.bak-gamescope-wizard` backup suffix: existing backups are found by it.
|
||||||
- `lib/*.sh` - one file per responsibility, each defining functions only
|
- `lib/*.sh` - one file per responsibility, each defining functions only
|
||||||
(no top-level side effects besides constants). See the table in
|
(no top-level side effects besides constants). See the table in
|
||||||
`TECHNICAL.md`.
|
`TECHNICAL.md`.
|
||||||
@@ -29,12 +48,21 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
`<id>_available` is checked via `component_available` (e.g. `machine`
|
`<id>_available` is checked via `component_available` (e.g. `machine`
|
||||||
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
|
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
|
||||||
`gaming` must stay first. Dependencies live in `toggle_component`.
|
`gaming` must stay first. Dependencies live in `toggle_component`.
|
||||||
|
- **NVIDIA** (`lib/nvidia.sh`): gamescope's session is broken on NVIDIA, so the SteamOS conversion (`gaming`, `boot`, `single`,
|
||||||
|
`glyphs`) is hidden there (unless already on) and `nvidia` + its sub-option `bigpicture` ("Gaming on NVIDIA") replace it:
|
||||||
|
Steam on the Plasma desktop, started at login. Never reintroduce a gamescope session for NVIDIA without re-testing on the
|
||||||
|
hardware (notes: steamify-cachyos-dev, `nvidia/`). It can't be tested without the hardware: keep the branch behind
|
||||||
|
`.no-release-yet` until it was.
|
||||||
- **Feature versions.** Set `FEATURE_VERSION[<id>]` (`lib/menu.sh`) to the new
|
- **Feature versions.** Set `FEATURE_VERSION[<id>]` (`lib/menu.sh`) to the new
|
||||||
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets
|
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets
|
||||||
one too): installs recorded with an older version
|
one too): installs recorded with an older version
|
||||||
are ticked and re-applied ("update" in the plan and the app). New default
|
are ticked and re-applied ("update" in the plan and the app). New default
|
||||||
options are ticked for installs whose parent (top-level: `gaming`) is on
|
options are ticked for installs whose parent (top-level: `gaming`) is on
|
||||||
(`feature_new`, shown as "new").
|
(`feature_new`, shown as "new"). New opt-in options (`NO_PRESELECT`) get the
|
||||||
|
same "new" badge without being ticked (`feature_new_optin`). Both count "Gaming
|
||||||
|
on NVIDIA" as a parent where the conversion isn't offered.
|
||||||
|
Options shown but left unticked in a confirmed run are recorded as
|
||||||
|
`off` (`feature_record_unticked`), or `feature_new` would tick them again.
|
||||||
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
|
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
|
||||||
for new changes.
|
for new changes.
|
||||||
- **Reversibility.** Every per-user KDE setting a component changes goes
|
- **Reversibility.** Every per-user KDE setting a component changes goes
|
||||||
@@ -47,6 +75,13 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
the entry point's `for lib in ...; do` source loop and wraps everything
|
the entry point's `for lib in ...; do` source loop and wraps everything
|
||||||
after that loop in `main()`. Keep that loop on one line, keep all logic in
|
after that loop in `main()`. Keep that loop on one line, keep all logic in
|
||||||
functions, and don't rely on `SCRIPT_DIR` for anything but sourcing.
|
functions, and don't rely on `SCRIPT_DIR` for anything but sourcing.
|
||||||
|
The workflow is one file for GitHub and a Gitea mirror (`github.server_url`): build and check run on both
|
||||||
|
(shellcheck through `ludeeus/action-shellcheck`, which brings its own binary; the artifact with
|
||||||
|
`upload-artifact@v4` on GitHub and `christopherhx/gitea-upload-artifact@v4` elsewhere: never mention
|
||||||
|
`upload-artifact@v3`, GitHub fails a workflow for a deprecated version even in a skipped step); the
|
||||||
|
release is `gh` on GitHub and `akkuman/gitea-release-action` on Gitea. A new release on GitHub also starts the
|
||||||
|
Steamify ISO's release (`gh workflow run iso-1-github-tag.yml` in steamify-cachyos-live-iso, secret
|
||||||
|
`ISO_DISPATCH_TOKEN`; skipped without it): the ISO repo's GitHub tag names it, the Gitea mirror builds it.
|
||||||
CI (`.github/workflows/bundle.yml`) publishes the bundle as release
|
CI (`.github/workflows/bundle.yml`) publishes the bundle as release
|
||||||
`v$VERSION` on pushes to `main`; an existing version is never overwritten,
|
`v$VERSION` on pushes to `main`; an existing version is never overwritten,
|
||||||
so bump `VERSION` for every release.
|
so bump `VERSION` for every release.
|
||||||
@@ -77,11 +112,56 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
+ subject; a commit can't contain its own hash, so fill it in with the
|
+ subject; a commit can't contain its own hash, so fill it in with the
|
||||||
next commit). The section heading must be `## <VERSION> - <date>`: CI
|
next commit). The section heading must be `## <VERSION> - <date>`: CI
|
||||||
cuts the release notes out of the changelog by that heading.
|
cuts the release notes out of the changelog by that heading.
|
||||||
- Every PR that should be released bumps `VERSION` and adds its section.
|
- **Branches: one release branch per version.** Work for a release goes
|
||||||
A push to `main` publishes release `v$VERSION` (tag + bundle + that
|
into `release/X.Y.Z` (e.g. `release/2.7.0`), branched from `main` when
|
||||||
|
that version starts. That branch bumps `VERSION` to `X.Y.Z` and adds the
|
||||||
|
`## X.Y.Z - <date>` changelog section (first commit on it).
|
||||||
|
- Every change gets its own branch from the release branch:
|
||||||
|
`feature/<name>` (new behaviour) or `bugfix/<name>` (a fix), and a
|
||||||
|
pull request **into the release branch**, not into `main`. Its commits
|
||||||
|
add their changelog lines to that version's section.
|
||||||
|
- When the release is done and tested (the test plan in
|
||||||
|
steamify-cachyos-dev, `TESTPLAN.md`), the release branch gets a pull
|
||||||
|
request **into `main`**. Merging it publishes the release (below).
|
||||||
|
- A fix for a released version: a new `release/X.Y.Z+1` from `main`,
|
||||||
|
with the `bugfix/` branch into it; don't reopen an old release branch.
|
||||||
|
- Up to the release branch, the agent manages it: create the
|
||||||
|
`feature/`/`bugfix/` branches, commit, push, and merge them into the
|
||||||
|
release branch itself once tested (with `gh`: `gh pr create --base
|
||||||
|
release/X.Y.Z` + `gh pr merge --merge`; no review needed). Without
|
||||||
|
`gh` (`command -v gh`), ask the user to install it (Ubuntu:
|
||||||
|
`sudo apt install gh`, then `gh auth login`) instead of working around
|
||||||
|
it. Only the
|
||||||
|
release branch's pull request into `main` is the user's: never commit
|
||||||
|
or merge to `main`, the user merges that one. Never work directly on
|
||||||
|
`main` or a `release/*` branch either: edits, even uncommitted ones,
|
||||||
|
go on a `feature/`/`bugfix/` branch (create it before editing; the
|
||||||
|
release branch only gets the version-bump commit and merged PRs).
|
||||||
|
Don't push a new `release/*` branch before it's needed: a push can
|
||||||
|
start CI builds (a dev ISO).
|
||||||
|
A machine without a git identity: pass the repo's author per command
|
||||||
|
(`git -c user.name=... -c user.email=... commit`, taken from `git
|
||||||
|
log`) instead of changing the git config.
|
||||||
|
A `.no-release-yet` file at the root of a feature/bugfix branch means
|
||||||
|
it must not be merged into the release branch yet (work in progress,
|
||||||
|
untested on hardware). When the feature is complete, ask the user
|
||||||
|
whether it may be released; on a yes, delete the file, then open the
|
||||||
|
pull request and merge it.
|
||||||
|
Keep a feature/bugfix branch up to date by merging (or rebasing on)
|
||||||
|
its release branch, the release branch by merging `main` when that
|
||||||
|
moved.
|
||||||
|
- A push to `main` publishes release `v$VERSION` (tag + bundle + that
|
||||||
changelog section) and marks it latest. An existing version is never
|
changelog section) and marks it latest. An existing version is never
|
||||||
overwritten: without a bump nothing is released, and pull requests show
|
overwritten: without a bump nothing is released, and pull requests show
|
||||||
a warning.
|
a warning when `VERSION` is a version that's already released (so a
|
||||||
|
release branch that forgot its bump shows it on every PR).
|
||||||
|
- A release that adds, removes or renames a menu row also retakes the
|
||||||
|
README screenshots (`assets/screenshot-menu-steam-machine.png` and
|
||||||
|
`assets/screenshot-menu-nvidia.png`, side by side in the README): the app from the branch on a Steam Machine and on a PC
|
||||||
|
with an NVIDIA card, every row visible and each window only as tall as its options need, the header showing the new
|
||||||
|
`VERSION`, 1600 px wide. Size the window with a KWin script (match the `steamify-ui` class exactly: a title match also hits
|
||||||
|
Konsole and editor windows) and crop the full-screen capture to the client rectangle; mind the display scale (the Steam
|
||||||
|
Machine runs at 1.75).
|
||||||
- Users install through `releases/latest/download/steamify.sh`
|
- Users install through `releases/latest/download/steamify.sh`
|
||||||
(GitHub's newest release). The `latest` tag and release follow the newest
|
(GitHub's newest release). The `latest` tag and release follow the newest
|
||||||
version tag too (moved, asset replaced, when a new version is released),
|
version tag too (moved, asset replaced, when a new version is released),
|
||||||
@@ -137,7 +217,9 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
`restart_plasmashell_if_stopped` (`lib/common.sh`).
|
`restart_plasmashell_if_stopped` (`lib/common.sh`).
|
||||||
- LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`,
|
- LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`,
|
||||||
not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets
|
not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets
|
||||||
`MAKE[0]="make KVERSION=${kernelver}"` (written before the AUR install).
|
`MAKE[0]="make KVERSION=${kernelver}"` (written before the AUR install;
|
||||||
|
someone's own override there is backed up, kept with ours appended, and
|
||||||
|
restored on disable).
|
||||||
Without it, other kernels build against the running kernel's tree and
|
Without it, other kernels build against the running kernel's tree and
|
||||||
fail (CachyOS kernels are clang-built; DKMS adds `LLVM=1` only for the
|
fail (CachyOS kernels are clang-built; DKMS adds `LLVM=1` only for the
|
||||||
target's tree). Headers for every installed kernel are installed first,
|
target's tree). Headers for every installed kernel are installed first,
|
||||||
@@ -188,26 +270,10 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
The start script closes its window after a 10-second countdown on success
|
The start script closes its window after a 10-second countdown on success
|
||||||
and waits for Enter after an error; it uses `pipefail`, or a failed
|
and waits for Enter after an error; it uses `pipefail`, or a failed
|
||||||
download would run an empty script and count as success.
|
download would run an empty script and count as success.
|
||||||
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): a sub-option of
|
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): retired in 2.2.0
|
||||||
`machine` (like `kpin` and `bios`), only on Fremont with the pinned kernel
|
(it needed the pinned kernel); only removal is left (see the power-off
|
||||||
(stays visible while on); unticking `kpin` unticks it. It needs someone at
|
fix below). `hdmi_enable` refuses. debugfs is root-only (glob it under
|
||||||
the screen. Terminal: every step needs a "y" within 15 s
|
sudo), and `edid_override` takes exactly `reset` with no newline.
|
||||||
(`WIZARD_HDMI_CONFIRM_SECONDS` for scripted tests). App: its own screen
|
|
||||||
(`hdmi-options`, `hdmi-try`, `hdmi-reset` backend commands, the EDID cached
|
|
||||||
in `$XDG_RUNTIME_DIR/steamify-hdmi`), then `apply --hdmi
|
|
||||||
<output>=<w>x<h>:<rates>`; without `--hdmi` the backend refuses it. debugfs is root-only
|
|
||||||
(glob it under sudo), and `edid_override` takes exactly `reset` with no
|
|
||||||
newline. Build the EDID from the DDC read, not from sysfs: a live override
|
|
||||||
replaces the kernel's copy. EDIDs are saved per display
|
|
||||||
(`steamify-<id>.bin`, id = EDID bytes 8-17, listed in
|
|
||||||
`/etc/steamify/hdmi-edid.conf`), never on the kernel command line (that
|
|
||||||
applied to any display on the port): `steamify-edid-hotplug` (boot unit +
|
|
||||||
udev drm hotplug rule) loads the connected display's file, else resets. It
|
|
||||||
records what's loaded in `/run/steamify-edid` before its own
|
|
||||||
`trigger_hotplug`, whose event runs it again. `hdmi_status` is on only
|
|
||||||
while the connected display is boosted; disable forgets only that display
|
|
||||||
(`kpin_disable` forgets all). The app manages the list (`hdmi-forget`,
|
|
||||||
`hdmiDisplays` in status). Pre-2.1.0 setups are moved by `hdmi_migrate`.
|
|
||||||
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
|
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
|
||||||
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
|
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
|
||||||
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
|
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
|
||||||
@@ -216,6 +282,11 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
kernel only pairs that with a named lookup ("Port C") when the CEC driver
|
kernel only pairs that with a named lookup ("Port C") when the CEC driver
|
||||||
registered first, which never happens since amdgpu loads from the
|
registered first, which never happens since amdgpu loads from the
|
||||||
initramfs. Without it `/dev/cec0` exists but stays at `f.f.f.f`.
|
initramfs. Without it `/dev/cec0` exists but stays at `f.f.f.f`.
|
||||||
|
The source comes from one raw.githubusercontent.com download, which GitHub rate-limits (HTTP 429 after a
|
||||||
|
day of test installs, per address): it's kept in `/var/cache/steamify/cros-ec-cec-<sha256:12>.c` and reused
|
||||||
|
while its checksum matches. The name carries the checksum, so a new pin (commit + SHA-256 in `lib/cec.sh`)
|
||||||
|
downloads its own file once and removes the old one. The repo is an unofficial mirror of Valve's kernel
|
||||||
|
without releases: if it ever disappears, ship the file with Steamify. The test VMs share the host's cache.
|
||||||
- Power-off fix (`poweroff`, `lib/fremont-poweroff.sh`, a default sub-option
|
- Power-off fix (`poweroff`, `lib/fremont-poweroff.sh`, a default sub-option
|
||||||
of `machine`): recent kernels (7.2 and the 6.x/7.0/7.1 updates with the
|
of `machine`): recent kernels (7.2 and the 6.x/7.0/7.1 updates with the
|
||||||
backport) keep the firmware's S4/S5 wake bit on GPIO
|
backport) keep the firmware's S4/S5 wake bit on GPIO
|
||||||
@@ -232,14 +303,64 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
`linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the
|
`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
|
compiler, never pass `LLVM=1`. Test shutdown on the real machine for every
|
||||||
new major kernel.
|
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
|
||||||
|
compares GitHub's newest release with the `seen` version every run records
|
||||||
|
(`notify_seen` in the entry point, only while it's on, with `frontend`: app or terminal, which Open Steamify starts again, in a `systemd-run --scope` since the check's own unit is stopped when it exits). 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
|
- 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
|
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,
|
`vram`/`vidmem` region (numbered too) of at least 2 GB, whatever the brand;
|
||||||
`vram_selectable`, faked with `WIZARD_VRAM_FAKE_NVIDIA=1`): CachyOS's `dmemcg-booster` (system + user service) and
|
greyed out with an NVIDIA card whose driver lists none, `vram_selectable`,
|
||||||
|
faked with `WIZARD_VRAM_FAKE_NVIDIA=1`; `WIZARD_VRAM_CAPACITY=<file>` reads
|
||||||
|
the regions from a copy): CachyOS's `dmemcg-booster` (system + user service) and
|
||||||
`plasma-foreground-booster`, like SteamOS 3.9's VRAM management. The
|
`plasma-foreground-booster`, like SteamOS 3.9's VRAM management. The
|
||||||
latter only starts with `kcgroupsrc [Foreground Booster] autostart=true`
|
latter only starts with `kcgroupsrc [Foreground Booster] autostart=true`
|
||||||
(set with `kset`). Disable removes only the packages it installed
|
(set with `kset`). Disable removes only the packages it installed
|
||||||
(`state_get vram installed_pkgs`).
|
(`state_get vram installed_pkgs`).
|
||||||
|
- Install-time mode (`steamify.sh --defaults`, 2.6.0): applies what the menu
|
||||||
|
would preselect, no menu or prompts (needs passwordless sudo). The Steam
|
||||||
|
Machine ISO ([steamify-cachyos-live-iso](https://github.com/theupriser/steamify-cachyos-live-iso))
|
||||||
|
runs it in the installer for a user who has never logged in: no session
|
||||||
|
bus, user systemd or plasmashell. So every `systemctl --user` goes through
|
||||||
|
`user_systemctl` (`lib/common.sh`; without a session only unit files change,
|
||||||
|
via `--root=/`), and the theme, when the layout is still `/etc/skel`'s,
|
||||||
|
removes it so Plasma builds Vapor's layout at the first login. What needs
|
||||||
|
that layout (single user's launcher) runs then, from a one-time autostart
|
||||||
|
(`lib/first-login.sh`, `--first-login`) that also opens the app.
|
||||||
|
`--options <id>,...` (exactly these on, the rest off; an item on brings its
|
||||||
|
parent) and `--boot gamescope|desktop` (desktop only with `gaming`) (2.7.0,
|
||||||
|
`defaults_options` in `lib/menu.sh`); the ISO's installer page passes them.
|
||||||
|
Items this PC can't use are left off with a warning, not an error.
|
||||||
|
`steamify.sh --boot gamescope|desktop` on its own (2.7.0, for scripts) only
|
||||||
|
switches `boot` on an installed conversion: `WANTED` is `CURRENT` plus
|
||||||
|
that, so no updates or removals a normal run would pick.
|
||||||
|
- What Steam's System settings show (SteamOS conversion, 2.9.0): the OS name
|
||||||
|
stays CachyOS's everywhere (legal clarity; Steam's OS Name is
|
||||||
|
`lsb_release -d`, not touched). `/etc/os-release` gets `VERSION_ID=steamos-X.Y`
|
||||||
|
(Steam's OS Version: Valve's newest jupiter repo; offline, the previous
|
||||||
|
value), `VERSION_CODENAME=steam-machine`, `VARIANT="Steamify <version>"` and
|
||||||
|
`VARIANT_ID=steamify-<version>` (Steam shows VARIANT_ID as OS Variant),
|
||||||
|
refreshed by `os_version_refresh` after every run that changes something; a
|
||||||
|
pacman hook (`zz-steamify-os-release`) runs after cachyos-hooks. KDE's About
|
||||||
|
this System would show VERSION_ID after the name: `kcm-about-distrorc`
|
||||||
|
`UseOSReleaseVersion=true` (kset) makes it show os-release's VERSION, which
|
||||||
|
CachyOS doesn't have. Never `NAME` or `PRETTY_NAME`: limine-snapper-sync
|
||||||
|
finds the boot entries by the OS name ("Target OS name ... not found in
|
||||||
|
/boot/limine.conf"); `/etc/default/limine` gets `TARGET_OS_NAME="CachyOS"`
|
||||||
|
when it has none, as a guard. The serial number (tmpfiles rule) is Steam
|
||||||
|
Machine support's, so Fremont only.
|
||||||
- `Relogin=true` means a gamescope that fails to start is relaunched in a
|
- `Relogin=true` means a gamescope that fails to start is relaunched in a
|
||||||
tight loop; keep that in mind when changing session handling.
|
tight loop; keep that in mind when changing session handling.
|
||||||
|
|
||||||
|
|||||||
+127
-1
@@ -5,9 +5,135 @@ All notable changes, per version and per commit. Versions follow
|
|||||||
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
||||||
one per merged pull request.
|
one per merged pull request.
|
||||||
|
|
||||||
|
## 2.10.0 - 2026-10-01
|
||||||
|
|
||||||
|
- `d6db4f6` **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture) starts it in Big Picture, unticked in its normal window, on an empty Plasma session so windows of the last session don't cover it. On a supported card (RTX 20 series or newer) it also sets the NVIDIA kernel parameters and early modules. The conversion and its options are hidden there unless already on**
|
||||||
|
- `d1373e2` **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion**
|
||||||
|
- `5811f5c` **fix: "Add as non-Steam game" isn't offered on NVIDIA PCs (shown when already on, so it can be turned off)**
|
||||||
|
- `83248a8` **feat: Extended controller support (extended_controller_support, off by default): the Xbox wireless dongle (xone) and Xbox controllers over Bluetooth with rumble and battery level (xpadneo), built for every installed kernel; off removes only what Steamify installed**
|
||||||
|
- `01a77b7` **feat: a new opt-in option (unticked by default) shows the "new" badge in the menu and the app**
|
||||||
|
- `f1983a9` **docs: README screenshots of the 2.10.0 app, side by side: on a Steam Machine and on a PC with an NVIDIA card**
|
||||||
|
|
||||||
|
## 2.9.6 - 2026-09-30
|
||||||
|
|
||||||
|
- **fix(ci): a dev ISO from a release branch is built with that branch's own Steamify (input steamify_ref of the ISO workflow), not the newest published release: release/2.9.6 kept producing 2.9.5 ISOs**
|
||||||
|
- **docs: changelog hashes for 2.9.6**
|
||||||
|
- `9a2965d` **fix(ci): the ISO workflow always runs from master; main asks for a release ISO, a release branch for a dev ISO (input `kind` of iso-1-github-tag.yml; feat/steamify is no longer needed)**
|
||||||
|
- `c42d545` **fix(ci): a version published from main starts a real ISO release, a release branch a dev ISO (it always started the dev one)**
|
||||||
|
- `b3bed43` **chore: Version 2.9.6**
|
||||||
|
|
||||||
|
## 2.9.5 - 2026-09-30
|
||||||
|
|
||||||
|
- `70ecb56` **ci: trigger a mirror sync**
|
||||||
|
- `a7276cf` **ci: sync the git.upriser.nl mirror on every branch and tag**
|
||||||
|
- `b3549d4` **ci: sync the mirror on release branch pushes too**
|
||||||
|
- `a232a55` **feat(ci): sync the git.upriser.nl mirror on every push to main and every tag (git-upriser-sync.yml calls the mirror-sync API: the Gitea mirror builds at once instead of at its interval; secrets GIT_UPRISER_URL and GIT_UPRISER_TOKEN, retried, skipped on Gitea itself)**
|
||||||
|
- `3676c64` **docs: link the renamed steamify-cachyos-live-iso repo (the release dispatches the ISO workflow iso-1-github-tag.yml there)**
|
||||||
|
- `971e792` **chore: Version 2.9.5**
|
||||||
|
|
||||||
|
## 2.9.4 - 2026-09-29
|
||||||
|
|
||||||
|
- `619ef1f` **docs: AGENTS.md and TECHNICAL.md: the CEC driver cache, the one workflow for GitHub and a Gitea mirror, and that a new release starts the Steamify ISO's release**
|
||||||
|
- `b624053` **chore: Version 2.9.4**
|
||||||
|
|
||||||
|
## 2.9.3 - 2026-09-29
|
||||||
|
|
||||||
|
- `7f5d29c` **feat(ci): a new Steamify release starts the Steamify ISO's release (a test build from the ISO repo's feat/steamify; secret ISO_DISPATCH_TOKEN)**
|
||||||
|
- `cfe3ce7` **chore: Version 2.9.3**
|
||||||
|
|
||||||
|
## 2.9.2 - 2026-09-29
|
||||||
|
|
||||||
|
- **feat(ci): a Gitea mirror publishes its own release per version (akkuman/gitea-release-action; never overwrites one, notes from the changelog, links to the mirror)**
|
||||||
|
- **fix(ci): the build also runs on a Gitea mirror (act_runner): shellcheck through ludeeus/action-shellcheck, which brings its own binary; the artifact goes up there with christopherhx/gitea-upload-artifact (GitHub refuses a workflow that mentions the deprecated upload-artifact@v3); releases are only published on GitHub**
|
||||||
|
- `fe4f3f4` **chore: Version 2.9.2**
|
||||||
|
|
||||||
|
## 2.9.1 - 2026-09-29
|
||||||
|
|
||||||
|
- `c87e6c7` **docs: changelog for the CEC driver cache**
|
||||||
|
- `fc05725` **fix: HDMI-CEC: an older cached CEC driver is removed when a new pin is downloaded**
|
||||||
|
- `90bc5e4` **fix: HDMI-CEC: Valve's CEC driver is downloaded once and kept in /var/cache/steamify (GitHub rate-limits repeated downloads with HTTP 429, which left CEC off); turning CEC on again needs no network**
|
||||||
|
- `d99ff65` **chore: Version 2.9.1**
|
||||||
|
|
||||||
|
## 2.9.0 - 2026-09-28
|
||||||
|
|
||||||
|
- `d30d505` **chore: Version 2.9.0**
|
||||||
|
- `66ea63c` **docs: KDE Plasma only: other desktops and display managers "are not supported" (README, was "don't work yet")**
|
||||||
|
- `9b86cd1` **docs: KDE Plasma only: other desktops don't work yet (README)**
|
||||||
|
- `d73e71d` **docs: README, TECHNICAL and TROUBLESHOOTING for 2.9.0: Steam's System settings, serial number and Wi-Fi backend, --defaults --list, the Steamify ISO**
|
||||||
|
- `a72d848` **feat: Steam's OS Version is the SteamOS release Steamify follows (steamos-3.9), its OS Codename steam-machine; About this System keeps "CachyOS Linux" (UseOSReleaseVersion)**
|
||||||
|
- `27a4070` **fix: Steam's OS Variant shows Steamify's version (VARIANT_ID=steamify-2.9.0; Steam shows VARIANT_ID, not VARIANT)**
|
||||||
|
- `44bfe07` **fix: No VERSION_ID in os-release: About this System said "CachyOS Linux 2.9.0"; the version is in VARIANT ("Steamify 2.9.0")**
|
||||||
|
- `435f69c` **feat: Steam's System settings: the OS name stays CachyOS (no renaming, also not in About this System), Steamify <version> as the variant, the SteamOS release it follows as the codename (steamos-3.9)**
|
||||||
|
- `ce71b34` **feat: Steam's System settings and About this System like on SteamOS: OS name "CachyOS with Steamify" (lsb-release, kept through CachyOS updates by a pacman hook), Steamify as OS variant and version (os-release VARIANT/VERSION_ID; NAME/PRETTY_NAME stay CachyOS's for limine, TARGET_OS_NAME pinned), "CachyOS Linux with Steamify" in About this System; on a Steam Machine the serial number and Steam's Wi-Fi backend setting (existing installs get it as an update)**
|
||||||
|
- `07a94cd` **fix(ui): The progress screen's ✓ and ! stand upright: only a dot on the circle spins while a step runs (they kept the spinner's last angle)**
|
||||||
|
|
||||||
|
## 2.8.0 - 2026-09-28
|
||||||
|
|
||||||
|
- **chore: Version 2.8.0**
|
||||||
|
- **feat: steamify.sh --defaults --list: what --defaults can set up here, as JSON, for an installer page**
|
||||||
|
|
||||||
|
## 2.7.0 - 2026-09-28
|
||||||
|
|
||||||
|
- `4f6e6fd` **feat: --defaults takes --skip <ids> and --boot gamescope|desktop, for the Steam Machine ISO's installer pages**
|
||||||
|
- `5bf9da4` **fix: HDMI-CEC enables cec-audio-control.socket (the TV remote's volume keys); existing installs get it as an update**
|
||||||
|
- `14aa7c0` **fix: the app's progress screen says "Boot into desktop", not "Boot into Boot into"**
|
||||||
|
- `c4ec755` **feat: --defaults --options <ids> (exactly these on) replaces --skip; steamify.sh --boot gamescope|desktop on its own switches where an installed conversion starts**
|
||||||
|
- `eadbdda` **docs: release branches (release/X.Y.Z, feature/ and bugfix/ branches into it, the release into main); CI builds release/** pushes**
|
||||||
|
- `bee18b7` **docs: the agent merges feature/bugfix branches into the release branch; the release into main stays the user's**
|
||||||
|
- `501c0ba` **docs: gh or git for merging into the release branch**
|
||||||
|
- `b1adacb` **docs: gh is required for the release-branch pull requests; ask the user to install it**
|
||||||
|
- `ced4a45` **chore: the v2 app preview is gone (it is the released app): no preview pre-release step in CI, no mention in steamify-app.sh**
|
||||||
|
- **fix: turning the theme off no longer brings back single user's launcher restriction when single user was turned off in between**
|
||||||
|
|
||||||
|
## 2.6.0 - 2026-09-27
|
||||||
|
|
||||||
|
- `b948c20` **feat: --defaults applies the default setup without the menu, also from an installer (no session): the base for a Steam Machine ISO**
|
||||||
|
|
||||||
|
## 2.5.2 - 2026-09-27
|
||||||
|
|
||||||
|
- `0c96bed` **docs: README screenshot at 2.5.2: everything on, nothing to update**
|
||||||
|
|
||||||
|
## 2.5.1 - 2026-09-27
|
||||||
|
|
||||||
|
- **fix: Add as non-Steam game asks Steam to close again every 15 s (a Steam that was still starting ignored it)**
|
||||||
|
- `3a0692f` **docs: README screenshot at 2.5.1 with Add as non-Steam game**
|
||||||
|
- `e86ddcc` **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**
|
||||||
|
- `32c33e4` **fix: An option left unticked stays off: it came back ticked as "new" on the next run (since 2.3.0)**
|
||||||
|
- `2dd0f3a` **feat: VRAM booster with NVIDIA says what to do: update, switch to the open driver (chwd command), or not supported by the card**
|
||||||
|
- `57995bc` **docs: VRAM booster works with NVIDIA's open kernel modules (driver 615+); only the closed driver is greyed out**
|
||||||
|
- `d57e8d8` **docs: AGENTS.md and changelog for the vidmem detection**
|
||||||
|
- `5a34633` **feat: VRAM booster also for drivers that name their region vidmem (or numbered), NVIDIA included once its driver registers it**
|
||||||
|
- `479b70b` **feat: Everything in the home folder under steamify (state, scripts, icon), moved once from the old name**
|
||||||
|
- `2e0881b` **fix: fill takes values literally; the notifier path is quoted in its unit**
|
||||||
|
- `cce31e1` **refactor: DKMS confs in patches/; an existing leds-valve DKMS override is kept and restored**
|
||||||
|
- `bd4fa3a` **refactor: The session sync and kernel headers scripts in patches/**
|
||||||
|
- `8ffa0f2` **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**
|
||||||
|
|
||||||
|
## 2.4.1 - 2026-09-27
|
||||||
|
|
||||||
|
- **chore: Version 2.4.1**
|
||||||
|
- `6f598d1` **docs: README screenshot shows every option**
|
||||||
|
- `9bbe5fa` **docs: Screenshot of the app's menu in the README**
|
||||||
|
|
||||||
|
## 2.4.0 - 2026-09-27
|
||||||
|
|
||||||
|
- `03a8b8c` **chore: Version 2.4.0**
|
||||||
|
- `e699a3f` **refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed**
|
||||||
|
|
||||||
## 2.3.0 - 2026-09-27
|
## 2.3.0 - 2026-09-27
|
||||||
|
|
||||||
- **feat: VRAM booster greyed out with an NVIDIA card, with why**
|
- `86b9744` **feat: VRAM booster greyed out with an NVIDIA card, with why**
|
||||||
- `b0947d7` **feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM**
|
- `b0947d7` **feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM**
|
||||||
- `3f97c85` **feat: VRAM booster for every supported GPU, not only the Steam Machine**
|
- `3f97c85` **feat: VRAM booster for every supported GPU, not only the Steam Machine**
|
||||||
- `fd162c1` **feat: New badge for default sub-options added in this version**
|
- `fd162c1` **feat: New badge for default sub-options added in this version**
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Steamify CachyOS
|
# Steamify CachyOS
|
||||||
|
|
||||||
|
The Steamify app's menu. On a Steam Machine (or any PC with an AMD or Intel
|
||||||
|
graphics card) it offers the full SteamOS conversion, on the left. On a PC with
|
||||||
|
an NVIDIA graphics card gamescope's gaming mode shows a corrupted picture, so the
|
||||||
|
menu offers **Gaming on NVIDIA** instead (Steam on the desktop, started at login,
|
||||||
|
optionally in Big Picture), on the right.
|
||||||
|
|
||||||
|
| On a Steam Machine | On a PC with an NVIDIA graphics card |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight
|
Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight
|
||||||
into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma
|
into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma
|
||||||
desktop and back whenever you like - just like on a Steam Deck or Valve's
|
desktop and back whenever you like - just like on a Steam Deck or Valve's
|
||||||
@@ -17,6 +27,16 @@ Steam Machine.
|
|||||||
on the desktop too.
|
on the desktop too.
|
||||||
- **Boot into: gamescope / desktop** - where the PC starts. Gaming mode is
|
- **Boot into: gamescope / desktop** - where the PC starts. Gaming mode is
|
||||||
the default.
|
the default.
|
||||||
|
- Steam's **System** settings show the SteamOS release Steamify follows
|
||||||
|
as OS Version (e.g. `steamos-3.9`), `steam-machine` as OS Codename and
|
||||||
|
Steamify's version as OS Variant. The OS name stays CachyOS
|
||||||
|
([details](TECHNICAL.md#what-steams-system-settings-show)).
|
||||||
|
- **NVIDIA graphics cards:** gamescope's gaming mode shows a corrupted
|
||||||
|
picture there (an open NVIDIA driver bug), so the conversion isn't
|
||||||
|
offered. **Gaming on NVIDIA** takes its place: Steam on the desktop,
|
||||||
|
installed if it's missing and started at login, in **Big Picture** or its
|
||||||
|
normal window as you choose
|
||||||
|
([details](TECHNICAL.md#gaming-on-nvidia)).
|
||||||
2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with
|
2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with
|
||||||
**Add to Steam** in right-click menus
|
**Add to Steam** in right-click menus
|
||||||
([details](TECHNICAL.md#steamos-desktop-look)).
|
([details](TECHNICAL.md#steamos-desktop-look)).
|
||||||
@@ -28,17 +48,33 @@ Steam Machine.
|
|||||||
5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the
|
5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the
|
||||||
launcher opens the newest Steamify app, so you don't need the install
|
launcher opens the newest Steamify app, so you don't need the install
|
||||||
command again. "Steamify Terminal" in the launcher opens the terminal menu.
|
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
|
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. Not offered on PCs with an NVIDIA graphics card, which have no gaming mode.
|
||||||
|
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
|
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
|
whose driver supports it (AMD, Intel, NVIDIA with its open kernel modules
|
||||||
with an NVIDIA card until NVIDIA's driver supports it
|
from driver 615) on kernel 7.2 or newer. With NVIDIA's closed driver it's
|
||||||
|
greyed out and says what to do: switch to the open driver when your card
|
||||||
|
supports it (RTX 20 series and newer; Steamify shows the command, it
|
||||||
|
doesn't switch drivers itself)
|
||||||
([details](TECHNICAL.md#vram-booster)).
|
([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
|
Machine) - use Steam with your TV's
|
||||||
remote, and the TV turns on and off with the PC. Needs a PC with CEC (like
|
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
|
the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA
|
||||||
included, don't have it. See [CEC.md](CEC.md) for which PCs do, and how to
|
included, don't have it. See [CEC.md](CEC.md) for which PCs do, and how to
|
||||||
add it to your own build ([details](TECHNICAL.md#hdmi-cec)).
|
add it to your own build ([details](TECHNICAL.md#hdmi-cec)).
|
||||||
|
9. **Extended controller support** (off by default) - the Xbox wireless
|
||||||
|
dongle (xone) and Xbox controllers over Bluetooth with rumble, the right
|
||||||
|
button mapping and the battery level (xpadneo). Both are installed from the
|
||||||
|
CachyOS repo and built for every installed kernel
|
||||||
|
([details](TECHNICAL.md#extended-controller-support)).
|
||||||
|
|
||||||
## Steam Machine
|
## Steam Machine
|
||||||
|
|
||||||
@@ -49,7 +85,8 @@ What Steamify can do on a Valve Steam Machine:
|
|||||||
([details](TECHNICAL.md#hdmi-cec)).
|
([details](TECHNICAL.md#hdmi-cec)).
|
||||||
- **Steam Machine support** - the front **LED bar** works, Steam's
|
- **Steam Machine support** - the front **LED bar** works, Steam's
|
||||||
**hardware settings** (fan, performance) work, and the power button puts
|
**hardware settings** (fan, performance) work, and the power button puts
|
||||||
it to sleep like a console.
|
it to sleep like a console. Steam's System settings show the serial
|
||||||
|
number, and Steam's **Wi-Fi backend** setting works.
|
||||||
- **Power-off fix** - on by default: shutting down really turns the Steam
|
- **Power-off fix** - on by default: shutting down really turns the Steam
|
||||||
Machine off, also with the newest CachyOS kernel
|
Machine off, also with the newest CachyOS kernel
|
||||||
([details](TECHNICAL.md#power-off-fix-steam-machine)).
|
([details](TECHNICAL.md#power-off-fix-steam-machine)).
|
||||||
@@ -60,6 +97,11 @@ What Steamify can do on a Valve Steam Machine:
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- CachyOS with the KDE Plasma desktop (the default CachyOS Desktop edition)
|
- CachyOS with the KDE Plasma desktop (the default CachyOS Desktop edition)
|
||||||
|
|
||||||
|
> **KDE Plasma only.** Steamify works with KDE Plasma and its login managers
|
||||||
|
> (plasma-login-manager or SDDM). Other desktops (GNOME, Hyprland, Cosmic,
|
||||||
|
> ...) and other display managers are not supported: the switching between gaming mode
|
||||||
|
> and the desktop, the theme and the shortcuts are all built for Plasma.
|
||||||
- Your normal user account, with permission to use `sudo`
|
- Your normal user account, with permission to use `sudo`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -70,6 +112,11 @@ Open **Konsole** on your Plasma desktop and run:
|
|||||||
curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
|
curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Installing CachyOS on a Steam Machine? The
|
||||||
|
[Steamify CachyOS ISO](https://github.com/theupriser/steamify-cachyos-live-iso)
|
||||||
|
has Steamify built into the installer: pick the options there, and the
|
||||||
|
first boot is already set up.
|
||||||
|
|
||||||
Prefer to look at the script first? Clone the repository and run
|
Prefer to look at the script first? Clone the repository and run
|
||||||
[`./steamify.sh`](steamify.sh) (keep the whole folder: it needs
|
[`./steamify.sh`](steamify.sh) (keep the whole folder: it needs
|
||||||
[`lib/`](lib/)).
|
[`lib/`](lib/)).
|
||||||
@@ -112,6 +159,14 @@ steamos-session-select persistent # start where you left off last time
|
|||||||
steamos-session-select oneshot # always start in gaming mode (default)
|
steamos-session-select oneshot # always start in gaming mode (default)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Where the PC starts after a restart can also be set from a script, once the
|
||||||
|
SteamOS conversion is on (only that changes; `sudo` may ask for your password):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
steamify.sh --boot desktop # start in the desktop from the next boot on
|
||||||
|
steamify.sh --boot gamescope # start in gaming mode again (default)
|
||||||
|
```
|
||||||
|
|
||||||
## Something wrong?
|
## Something wrong?
|
||||||
|
|
||||||
Stuck on a black screen in gaming mode: press **Ctrl+Alt+F3**, log in, and run
|
Stuck on a black screen in gaming mode: press **Ctrl+Alt+F3**, log in, and run
|
||||||
|
|||||||
+221
-10
@@ -14,7 +14,7 @@ and turns on what you ticked. To be able to undo:
|
|||||||
off.
|
off.
|
||||||
- **KDE settings** in your home directory are recorded with their previous
|
- **KDE settings** in your home directory are recorded with their previous
|
||||||
value the first time the wizard changes them, in an undo journal under
|
value the first time the wizard changes them, in an undo journal under
|
||||||
`~/.local/state/cachyos-gamescope-boot/`. Turning the component off writes
|
`~/.local/state/steamify/`. Turning the component off writes
|
||||||
the old values back (or removes keys that didn't exist before). Setups
|
the old values back (or removes keys that didn't exist before). Setups
|
||||||
made by older versions of the script, without a journal, fall back to
|
made by older versions of the script, without a journal, fall back to
|
||||||
KDE's defaults.
|
KDE's defaults.
|
||||||
@@ -43,6 +43,14 @@ user wallet, with anything saved in it meanwhile, is kept as
|
|||||||
`kdewallet.kwl.steamify-single-user` and used again the next time it's on.
|
`kdewallet.kwl.steamify-single-user` and used again the next time it's on.
|
||||||
Passwords aren't shared between the two wallets.
|
Passwords aren't shared between the two wallets.
|
||||||
|
|
||||||
|
**Without the conversion (NVIDIA PCs, where gamescope's session is broken)** nothing else logs in by itself, so single user mode
|
||||||
|
does that too: `single_login_enable` (`lib/single-user.sh`) switches to SDDM and writes
|
||||||
|
`/etc/sddm.conf.d/zzz-steamify-autologin.conf` (`User=`, `Session=plasma.desktop`, `Relogin=true`; named to sort after
|
||||||
|
`steam-set-session`'s `zz-steamos-autologin.conf`, which may still say gamescope from an earlier conversion), and drops any
|
||||||
|
`[Autologin]` from `/etc/sddm.conf`. Turning it off removes the file and goes back to plasma-login-manager. With the
|
||||||
|
conversion on, the conversion does the login as before. Ticking single user mode only ticks the conversion where the
|
||||||
|
conversion is offered.
|
||||||
|
|
||||||
**SDDM** is what SteamOS uses, and CachyOS's `steam-set-session` supports it
|
**SDDM** is what SteamOS uses, and CachyOS's `steam-set-session` supports it
|
||||||
directly: it writes `/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM
|
directly: it writes `/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM
|
||||||
honours. The script installs and enables `sddm` (active from the next boot),
|
honours. The script installs and enables `sddm` (active from the next boot),
|
||||||
@@ -111,6 +119,9 @@ Turning it off restores `/etc/plasmalogin.conf` from its backup, removes the
|
|||||||
SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and
|
SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and
|
||||||
switches back to plasma-login-manager.
|
switches back to plasma-login-manager.
|
||||||
|
|
||||||
|
The conversion also sets what Steam's System settings show, see
|
||||||
|
[below](#what-steams-system-settings-show).
|
||||||
|
|
||||||
**Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"`
|
**Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"`
|
||||||
in `~/.config/environment.d/` (and gamescope-session's own environment
|
in `~/.config/environment.d/` (and gamescope-session's own environment
|
||||||
file), which makes Steam show Steam Deck button glyphs in gaming mode.
|
file), which makes Steam show Steam Deck button glyphs in gaming mode.
|
||||||
@@ -139,6 +150,33 @@ taken from the newest `steamdeck-kde-presets` on Valve's SteamOS mirror
|
|||||||
Turning it off restores your previous look and panel layout, and removes `cachyos-vapor` again
|
Turning it off restores your previous look and panel layout, and removes `cachyos-vapor` again
|
||||||
if the wizard installed it (and nothing else, like `cachyos-handheld`, needs it).
|
if the wizard installed it (and nothing else, like `cachyos-handheld`, needs it).
|
||||||
|
|
||||||
|
## What Steam's System settings show
|
||||||
|
|
||||||
|
With the SteamOS conversion on, `/etc/os-release` gets:
|
||||||
|
|
||||||
|
| Key | Value | Shown in Steam as |
|
||||||
|
|---|---|---|
|
||||||
|
| `VERSION_ID` | `steamos-3.9`: the newest SteamOS release in Valve's repo (offline: the previous value) | OS Version |
|
||||||
|
| `VERSION_CODENAME` | `steam-machine` | OS Codename |
|
||||||
|
| `VARIANT` / `VARIANT_ID` | `Steamify 2.9.0` / `steamify-2.9.0` | OS Variant (`VARIANT_ID`) |
|
||||||
|
|
||||||
|
The OS name stays CachyOS everywhere: Steam's OS Name comes from
|
||||||
|
`lsb_release -d`, which isn't touched, and `NAME`/`PRETTY_NAME` stay
|
||||||
|
CachyOS's. limine-snapper-sync finds the boot entries by that name, so
|
||||||
|
changing it would break snapshot boot entries; `/etc/default/limine` gets
|
||||||
|
`TARGET_OS_NAME="CachyOS"` as a guard when it has none.
|
||||||
|
|
||||||
|
KDE's About this System would show `VERSION_ID` after the name
|
||||||
|
("CachyOS Linux steamos-3.9"); `UseOSReleaseVersion=true` in
|
||||||
|
`~/.config/kcm-about-distrorc` makes it show `VERSION`, which CachyOS doesn't
|
||||||
|
have, so it keeps saying "CachyOS Linux".
|
||||||
|
|
||||||
|
CachyOS's own hooks (`cachyos-hooks`) reset os-release on updates of
|
||||||
|
`filesystem`; the pacman hook `zz-steamify-os-release` runs after them and
|
||||||
|
sets the keys again (`/usr/local/libexec/steamify-os-release`). The values are
|
||||||
|
refreshed after every Steamify run that changes something. Turning the
|
||||||
|
conversion off removes the hook and runs CachyOS's `cachyos-branding` again.
|
||||||
|
|
||||||
## Front LED bar (Steam Machine)
|
## Front LED bar (Steam Machine)
|
||||||
|
|
||||||
The **Steam Machine support** component, only shown on Fremont hardware
|
The **Steam Machine support** component, only shown on Fremont hardware
|
||||||
@@ -156,7 +194,14 @@ The **Steam Machine support** component, only shown on Fremont hardware
|
|||||||
- installs and enables `steamos-manager`, the service Steam in gaming mode
|
- installs and enables `steamos-manager`, the service Steam in gaming mode
|
||||||
uses for hardware settings (fan, performance, and the HDMI-CEC settings
|
uses for hardware settings (fan, performance, and the HDMI-CEC settings
|
||||||
when [HDMI-CEC](#hdmi-cec) is on); it recognises the Steam Machine from its
|
when [HDMI-CEC](#hdmi-cec) is on); it recognises the Steam Machine from its
|
||||||
DMI data.
|
DMI data;
|
||||||
|
- makes the serial number readable for Steam, which shows it in its System
|
||||||
|
settings (`/etc/tmpfiles.d/steamify-serial.conf`: `product_serial` is
|
||||||
|
root-only by default);
|
||||||
|
- makes Steam's Wi-Fi backend setting work: steamos-manager switches it in
|
||||||
|
`/etc/NetworkManager/conf.d/99-valve-wifi-backend.conf`, which is created
|
||||||
|
with `wifi.backend=wpa_supplicant` (NetworkManager's default) when no
|
||||||
|
backend is set anywhere yet.
|
||||||
|
|
||||||
Turning it off removes all of that again (the AUR helper is kept).
|
Turning it off removes all of that again (the AUR helper is kept).
|
||||||
|
|
||||||
@@ -169,6 +214,53 @@ or upgraded. A newly added kernel doesn't come with its headers, so
|
|||||||
`ensure-kernel-headers.service` checks at every boot and installs any
|
`ensure-kernel-headers.service` checks at every boot and installs any
|
||||||
missing `-headers` package, which makes DKMS build the driver for it.
|
missing `-headers` package, which makes DKMS build the driver for it.
|
||||||
|
|
||||||
|
## Extended controller support
|
||||||
|
|
||||||
|
`extended_controller_support` (`lib/controllers.sh`, top-level, off by default, 2.10.0) installs two kernel drivers from CachyOS's own
|
||||||
|
repo (no AUR): `xone-dkms` with `xone-dongle-firmware` for the Xbox wireless dongle (the kernel has no driver for it; wired
|
||||||
|
Xbox controllers go through the same driver), and `xpadneo-dkms` for Xbox One/Series controllers over Bluetooth (without it
|
||||||
|
they work as plain gamepads but miss rumble including the triggers, the right button mapping and the battery level). Both are
|
||||||
|
DKMS modules, built for every installed kernel, so `install_kernel_headers` (`lib/steam-machine.sh`) makes sure each kernel has its
|
||||||
|
headers first. A PC that already has the AUR's `xone-dkms-git` keeps it: it provides the same driver and conflicts with the repo's
|
||||||
|
`xone-dkms`. Status is the three packages being installed. Off removes only what Steamify installed (state
|
||||||
|
`extended_controller_support`, `installed_pkgs`); the headers stay. The drivers load when the device shows up: the dongle is
|
||||||
|
plugged in again, or the controller reconnected. `tests/controllers-test.sh` tests it with a stub `pacman`.
|
||||||
|
|
||||||
|
## Gaming on NVIDIA
|
||||||
|
|
||||||
|
gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards: NVIDIA's open bug 5240452 (flicker and
|
||||||
|
artifacts in the DRM session at modes above 2560x1440@120, seen up to driver 595 and on RTX 3070 Ti, 4090, 5090 and 5080;
|
||||||
|
no fix known). The kernel parameters `nvidia-drm.modeset=1`/`fbdev=1` and loading the NVIDIA modules early don't change it
|
||||||
|
(the driver already reports both `Y`), and gamescope started nested inside KDE artifacts too when KWin hands its fullscreen
|
||||||
|
window straight to the display. What does work on an RTX 5080: Steam's Big Picture (`steam -gamepadui`) as a normal window of
|
||||||
|
the Plasma desktop session, smooth and clean.
|
||||||
|
|
||||||
|
So with an NVIDIA GPU (PCI vendor 0x10de, display class, its driver providing `nvidia_drm`: `nvidia_present`) the SteamOS
|
||||||
|
conversion (`gaming`, and its options `boot`, `single`, `glyphs`) isn't offered, unless it is already on (so it can be turned
|
||||||
|
off). `nvidia` ("Gaming on NVIDIA", `lib/nvidia.sh`) takes its place, hidden while the conversion is on (both would start
|
||||||
|
Steam at login): it installs `steam` when missing (recorded in state `nvidia`, removed again only then) and enables the user
|
||||||
|
unit `steamify-steam-autostart.service` (`services/`, `ExecStart=/usr/bin/steam @ARGS@`, Plasma only). Its sub-option
|
||||||
|
`bigpicture` ("Steam starts in Big Picture", ticked along with it) rewrites the unit with `-gamepadui` and sets
|
||||||
|
`ksmserverrc [General] loginMode=emptySession` (`kset`, undone with `krevert bigpicture`), so windows of the last session
|
||||||
|
(Discord, a browser) aren't restored on top of Big Picture; unticked, Steam starts in its normal window. The PC always boots into the desktop: there is no gamescope session. Takes effect at the next
|
||||||
|
login.
|
||||||
|
|
||||||
|
On a supported card (RTX 20 series or newer: not on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids`, the VRAM booster's
|
||||||
|
check, `vram_nvidia_legacy_id`; `nvidia_supported`) enabling it also does what NVIDIA's DRM stack wants, though it didn't cure
|
||||||
|
gamescope: `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` on the kernel command line (Limine, systemd-boot or GRUB; the file is
|
||||||
|
backed up first; on Limine into the existing `KERNEL_CMDLINE[default]="..."` or `+="..."` line, never an extra appended line,
|
||||||
|
which ends up as text; the kernel treats `nvidia_drm` and `nvidia-drm` alike) and
|
||||||
|
`/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)`), then the initramfs
|
||||||
|
and boot entries are rebuilt and checked for the parameters. The early-load drop-in is only there while every installed kernel
|
||||||
|
has the NVIDIA modules (mkinitcpio fails on a missing module, and `limine-mkinitcpio` then skips that kernel's boot entry):
|
||||||
|
a pacman hook (`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, after DKMS, before the initramfs is built) runs
|
||||||
|
`/usr/local/libexec/steamify-nvidia-initramfs` at every kernel or driver change and decides again. Turning it off removes
|
||||||
|
all of it. `tests/nvidia-hardware-test.sh check|apply|visual` is for a real NVIDIA PC (writes `~/steamify-nvidia-report.txt`). `tests/nvidia-test.sh` tests it against a fake GPU, a stub `pacman` and `systemctl`, and a temp home.
|
||||||
|
|
||||||
|
Tried and dropped (notes in steamify-cachyos-dev, `nvidia/HARDWARE-RESULTS.md`): a gamescope session that opens Big Picture in Plasma (it needed the `start-gamescope-session`
|
||||||
|
command shadowed in `/usr/local/bin`, and showed the desktop for a few seconds, then a Steam window with a black border);
|
||||||
|
a KWin session without Plasma (the mouse stuttered every second or two).
|
||||||
|
|
||||||
## HDMI-CEC
|
## HDMI-CEC
|
||||||
|
|
||||||
The **HDMI-CEC** item (on every PC; ticked by default only on a Steam
|
The **HDMI-CEC** item (on every PC; ticked by default only on a Steam
|
||||||
@@ -180,7 +272,9 @@ SHA-256 in Valve's package index:
|
|||||||
- `cecd`, Valve's CEC daemon (a user service started with the desktop and
|
- `cecd`, Valve's CEC daemon (a user service started with the desktop and
|
||||||
gaming mode): TV remote keys become normal key presses (arrows, Enter,
|
gaming mode): TV remote keys become normal key presses (arrows, Enter,
|
||||||
Back), and it turns the TV on and off with the PC;
|
Back), and it turns the TV on and off with the PC;
|
||||||
- `cec-audio-control`: volume of the TV or receiver;
|
- `cec-audio-control`: volume of the TV or receiver (the TV remote's volume
|
||||||
|
keys); its user socket is enabled by Steamify, since SteamOS enables it
|
||||||
|
through a preset that pacman doesn't apply;
|
||||||
- `inputattach-cec-units` (with `linuxconsole` from CachyOS for
|
- `inputattach-cec-units` (with `linuxconsole` from CachyOS for
|
||||||
`inputattach`): attaches USB CEC adapters (Pulse-Eight, RainShadow).
|
`inputattach`): attaches USB CEC adapters (Pulse-Eight, RainShadow).
|
||||||
|
|
||||||
@@ -236,17 +330,78 @@ 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
|
power-off fix as a new default sub-option (see feature versions below), so a
|
||||||
normal run adds it.
|
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/<id>/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`:
|
||||||
|
`<account>=<appid> 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)
|
||||||
|
installs `patches/steamify-notifier.py` as
|
||||||
|
`~/.local/share/steamify/bin/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
|
## VRAM booster
|
||||||
|
|
||||||
SteamOS 3.9 manages the dGPU's VRAM per cgroup: the game in front is
|
SteamOS 3.9 manages the dGPU's VRAM per cgroup: the game in front is
|
||||||
protected, background apps are evicted first. Without it a game that needs
|
protected, background apps are evicted first. Without it a game that needs
|
||||||
most of the VRAM (8 GB on the Steam Machine) can be pushed into system RAM
|
most of the VRAM (8 GB on the Steam Machine) can be pushed into system RAM
|
||||||
by the desktop and other apps, and stutter. The kernel side is the `dmem`
|
by the desktop and other apps, and stutter. The kernel side is the `dmem`
|
||||||
cgroup controller (7.2); amdgpu and Intel's xe register their VRAM with it,
|
cgroup controller (7.2); amdgpu and Intel's xe register their VRAM with it
|
||||||
NVIDIA's driver doesn't: with an NVIDIA card the option is shown greyed
|
(`drm/<pci>/vram`), NVIDIA's open kernel modules from driver 615 too
|
||||||
out, with why (`WIZARD_VRAM_FAKE_NVIDIA=1` fakes that for tests). Otherwise
|
(`nvidia/<pci>/vidmem`); `dmemcg-booster` protects every region it lists,
|
||||||
|
whatever its name. NVIDIA's closed modules don't: with such a card the option is shown greyed
|
||||||
|
out with what to do (`vram_nvidia_case`): open modules older than 615 ->
|
||||||
|
update; the closed driver on a card the open one supports (not in chwd's
|
||||||
|
legacy lists `/var/lib/chwd/ids/nvidia-{580,470,390}.ids`) -> the `chwd`
|
||||||
|
commands to switch, never switched by Steamify itself (a failed switch
|
||||||
|
means a black screen, and it can't be tested here); a legacy card or
|
||||||
|
nouveau -> not supported. Shown greyed
|
||||||
|
out, with why, until its driver lists a region: then it's offered like any
|
||||||
|
other (`WIZARD_VRAM_FAKE_NVIDIA=1` fakes the grey-out, `WIZARD_VRAM_CAPACITY=<file>`
|
||||||
|
reads the regions from a copy, for tests). Otherwise
|
||||||
the **VRAM booster** option is only shown when
|
the **VRAM booster** option is only shown when
|
||||||
`/sys/fs/cgroup/dmem.capacity` lists a VRAM region of at least 2 GB (a
|
`/sys/fs/cgroup/dmem.capacity` lists a VRAM region (`vram`, or `vidmem`
|
||||||
|
as other drivers name it, numbered with several) of at least 2 GB (a
|
||||||
dedicated GPU; an integrated one registers a small carve-out) or it's already on, and is
|
dedicated GPU; an integrated one registers a small carve-out) or it's already on, and is
|
||||||
ticked by default; new in 2.3.0, so setups with the SteamOS conversion on
|
ticked by default; new in 2.3.0, so setups with the SteamOS conversion on
|
||||||
get it ticked. CachyOS packages the userspace side, which it installs:
|
get it ticked. CachyOS packages the userspace side, which it installs:
|
||||||
@@ -269,7 +424,7 @@ worth it if the booster itself turns out to cause stutter.
|
|||||||
Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`):
|
Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`):
|
||||||
the Steamify version in which what it sets up last changed (2.1.0 for
|
the Steamify version in which what it sets up last changed (2.1.0 for
|
||||||
everything that hasn't changed since). After a component is turned on
|
everything that hasn't changed since). After a component is turned on
|
||||||
successfully, that version is recorded in `~/.local/state/cachyos-gamescope-boot/features.state`
|
successfully, that version is recorded in `~/.local/state/steamify/features.state`
|
||||||
(turning it off records `off`). Whether a component is on is always checked
|
(turning it off records `off`). Whether a component is on is always checked
|
||||||
on the system itself; the version only decides about updates:
|
on the system itself; the version only decides about updates:
|
||||||
|
|
||||||
@@ -323,6 +478,31 @@ To walk through it without flashing anything, run the wizard with
|
|||||||
`WIZARD_BIOS_DRY_RUN=1`: it downloads and checks the package and shows both
|
`WIZARD_BIOS_DRY_RUN=1`: it downloads and checks the package and shows both
|
||||||
warnings, skips fwupd's device check, and only prints the install command.
|
warnings, skips fwupd's device check, and only prints the install command.
|
||||||
|
|
||||||
|
## Where Steamify keeps its files
|
||||||
|
|
||||||
|
In the home folder, everything is under `steamify`:
|
||||||
|
|
||||||
|
| Where | What |
|
||||||
|
|---|---|
|
||||||
|
| `~/.local/state/steamify/` | State (`*.state`), undo journals (`*.journal`), the saved panel layout (`theme-layout/`) |
|
||||||
|
| `~/.local/share/steamify/app/` | The app (`steamify-app.sh` downloads it here) |
|
||||||
|
| `~/.local/share/steamify/bin/` | The shortcut's start scripts (`run-app`, `run-wizard`) and `steamify-notifier` |
|
||||||
|
| `~/.local/share/icons/hicolor/scalable/apps/steamify.svg` | The shortcut's icon |
|
||||||
|
| `~/.local/share/applications/steamify-*.desktop` | The launcher entries |
|
||||||
|
| `~/.config/systemd/user/` | `steamify-update-check.*`, `steam-desktop-autostart.service` |
|
||||||
|
|
||||||
|
System-wide: `/usr/local/lib/steamify/` (kernel headers script),
|
||||||
|
`/usr/src/steamify-*` (DKMS modules), `/var/cache/steamify/kernel` (the old
|
||||||
|
kernel pin's packages), and `*.bak-gamescope-wizard` backups next to the
|
||||||
|
system files that were edited (that name stays: it's how they're found again).
|
||||||
|
|
||||||
|
Before 2.5.0 the home folder paths used the project's old name
|
||||||
|
(`cachyos-gamescope-boot`). `migrate_layout` (`lib/state.sh`) moves them at
|
||||||
|
every start of a newer version, once, and leaves symlinks under the old names
|
||||||
|
so an older release still finds the same state; it fixes the paths in the
|
||||||
|
launcher entries and the update check's unit, and merges when both exist.
|
||||||
|
The headers script moves when Steam Machine support is set up again.
|
||||||
|
|
||||||
## Manual session control
|
## Manual session control
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -332,6 +512,31 @@ steamos-session-select persistent # remember the last-used session across reboo
|
|||||||
steamos-session-select oneshot # always start in gamescope (default, like a Deck)
|
steamos-session-select oneshot # always start in gamescope (default, like a Deck)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`steamify.sh --boot gamescope|desktop` sets where the PC starts after a
|
||||||
|
restart (the menu's Boot into row) from a script: it needs the SteamOS
|
||||||
|
conversion and changes nothing else, not even the updates a normal run would
|
||||||
|
apply. Exit code 1 without the conversion or with another value.
|
||||||
|
|
||||||
|
## Install-time mode (`--defaults`)
|
||||||
|
|
||||||
|
For an installer (the Steam Machine ISO), without a session or prompts
|
||||||
|
(passwordless sudo required):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
steamify.sh --defaults # what the menu would preselect
|
||||||
|
steamify.sh --defaults --options gaming,theme,cec,machine --boot desktop
|
||||||
|
steamify.sh --defaults --list # what --defaults can set up here, as JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
`--options` turns on exactly the listed items (the menu's ids: `gaming`,
|
||||||
|
`theme`, `glyphs`, `single`, `launcher`, `notify`, `vram`, `cec`, `machine`,
|
||||||
|
`poweroff`), everything else off; an item that's on brings its parent
|
||||||
|
(single user the conversion, the power-off fix Steam Machine support). An
|
||||||
|
item this PC can't use (e.g. `vram` without a VRAM region) is left off with
|
||||||
|
a warning. `--boot desktop` needs `gaming`. Unknown options, ids, `boot` or
|
||||||
|
`bios` in the list: exit code 1 before anything changes. What needs a
|
||||||
|
Plasma session waits for the first login (`lib/first-login.sh`).
|
||||||
|
|
||||||
With `Relogin=true`, a gamescope session that fails to start is restarted
|
With `Relogin=true`, a gamescope session that fails to start is restarted
|
||||||
immediately, which can turn into a loop - see
|
immediately, which can turn into a loop - see
|
||||||
[TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the way out.
|
[TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the way out.
|
||||||
@@ -357,10 +562,16 @@ immediately, which can turn into a loop - see
|
|||||||
| `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository |
|
| `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/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/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/first-login.sh` | `--defaults` without a session (the Steam Machine ISO's installer): the one-time first-login step that sets up single user's launcher on Plasma's new layout and opens the app |
|
||||||
|
| `lib/controllers.sh` | Extended controller support: Xbox wireless dongle (xone) and Xbox Bluetooth (xpadneo) from the CachyOS repo |
|
||||||
|
| `lib/nvidia.sh` | Gaming on NVIDIA: Steam on the desktop, started at login, optionally in Big Picture (replaces the SteamOS conversion there) |
|
||||||
| `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) |
|
| `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 |
|
| `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` |
|
| `.github/workflows/bundle.yml` | Builds and checks it on every push (GitHub and a Gitea mirror); publishes it on `main`, and starts the Steamify ISO's release |
|
||||||
|
|
||||||
The single-file version is generated: on every push to `main`, GitHub
|
The single-file version is generated: on every push to `main`, GitHub
|
||||||
Actions runs `.github/tools/bundle.sh`, checks the result with `bash -n` and
|
Actions runs `.github/tools/bundle.sh`, checks the result with `bash -n` and
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ restarted right away (`Relogin=true`), which is why it can loop.
|
|||||||
Run the wizard again and press `a` in the menu to re-apply everything that's
|
Run the wizard again and press `a` in the menu to re-apply everything that's
|
||||||
on (also useful after a CachyOS update). To undo a part, untick it.
|
on (also useful after a CachyOS update). To undo a part, untick it.
|
||||||
|
|
||||||
|
## "Target OS name ... not found in /boot/limine.conf"
|
||||||
|
|
||||||
|
limine-snapper-sync finds the boot entries by the OS name. Steamify never
|
||||||
|
changes that name (it only adds version keys to `/etc/os-release`), and pins
|
||||||
|
`TARGET_OS_NAME="CachyOS"` in `/etc/default/limine`. If you see this message,
|
||||||
|
check that `PRETTY_NAME` in `/etc/os-release` still says `CachyOS`, or set
|
||||||
|
`TARGET_OS_NAME` in `/etc/default/limine` to the name your boot entries use.
|
||||||
|
|
||||||
## The LED bar on the Steam Machine stays dark
|
## The LED bar on the Steam Machine stays dark
|
||||||
|
|
||||||
Restart once more; a freshly installed driver often needs a reboot. The menu
|
Restart once more; a freshly installed driver often needs a reboot. The menu
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
+21
-51
@@ -8,16 +8,6 @@
|
|||||||
# turns on the listed components and off the others (like ticking them
|
# turns on the listed components and off the others (like ticking them
|
||||||
# in the menu), then streams one JSON object per line: plan, start,
|
# in the menu), then streams one JSON object per line: plan, start,
|
||||||
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
|
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
|
||||||
# steamify.sh --backend apply ... --hdmi <output>=<w>x<h>:<rate>,... hdmi
|
|
||||||
# HDMI refresh boost with the rates the app confirmed.
|
|
||||||
# steamify.sh --backend hdmi-options
|
|
||||||
# hdmi-options event: every HDMI output, its mode and rates to offer.
|
|
||||||
# steamify.sh --backend hdmi-try <connector> <w> <h> <hz> [<rate>...]
|
|
||||||
# steamify.sh --backend hdmi-reset <connector> <w> <h> <hz>
|
|
||||||
# steamify.sh --backend hdmi-forget <display id>
|
|
||||||
# removes a saved display (hdmi-forgotten event); status lists them.
|
|
||||||
# switch to a rate live (hdmi-tried event), or back to the display's
|
|
||||||
# own EDID (hdmi-reset event); the app asks in between.
|
|
||||||
# steamify.sh --backend bios-prepare | bios-flash
|
# steamify.sh --backend bios-prepare | bios-flash
|
||||||
# the BIOS update in two steps, so the app shows both warnings between
|
# the BIOS update in two steps, so the app shows both warnings between
|
||||||
# them (bios-ready event: current, newest, checksum, compatible).
|
# them (bios-ready event: current, newest, checksum, compatible).
|
||||||
@@ -63,8 +53,12 @@ backend_status() {
|
|||||||
items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)"
|
items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)"
|
||||||
items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 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+=",\"update\":$(feature_outdated "$c" && echo true || echo false)"
|
||||||
items+=",\"new\":$(feature_new "$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)}"
|
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
|
done
|
||||||
local cec="" f
|
local cec="" f
|
||||||
for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done
|
for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done
|
||||||
@@ -72,20 +66,13 @@ backend_status() {
|
|||||||
if bios_available; then
|
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)}"
|
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
|
fi
|
||||||
# HDMI refresh boost's saved displays; "active" = connected and boosted.
|
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \
|
||||||
local hdmi_list="" active hid hname hmode hrates
|
|
||||||
active=" $(hdmi_active_ids | tr '\n' ' ') "
|
|
||||||
while IFS=$'\t' read -r hid hname hmode hrates; do
|
|
||||||
[[ -n "$hid" ]] || continue
|
|
||||||
hdmi_list+="${hdmi_list:+,}{\"id\":$(json_str "$hid"),\"name\":$(json_str "$hname"),\"mode\":$(json_str "$hmode"),\"rates\":$(json_str "$hrates"),\"active\":$([[ "$active" == *" $hid "* ]] && echo true || echo false)}"
|
|
||||||
done < <(hdmi_saved)
|
|
||||||
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"hdmiDisplays":[%s],"items":[%s]}\n' \
|
|
||||||
"$(json_str "$VERSION")" "$first_run" \
|
"$(json_str "$VERSION")" "$first_run" \
|
||||||
"$(detect_valve_fremont && echo true || echo false)" \
|
"$(detect_valve_fremont && echo true || echo false)" \
|
||||||
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
|
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
|
||||||
"$(json_str "$cec")" \
|
"$(json_str "$cec")" \
|
||||||
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
|
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
|
||||||
"$bios" "$hdmi_list" "$items"
|
"$bios" "$items"
|
||||||
}
|
}
|
||||||
|
|
||||||
backend_run_component() {
|
backend_run_component() {
|
||||||
@@ -146,7 +133,6 @@ backend_apply() {
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--reapply) reapply=true ;;
|
--reapply) reapply=true ;;
|
||||||
--boot) boot="$2"; shift ;;
|
--boot) boot="$2"; shift ;;
|
||||||
--hdmi) HDMI_CHOICE+=("$2"); shift ;;
|
|
||||||
*) want+=("$1") ;;
|
*) want+=("$1") ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -160,20 +146,28 @@ backend_apply() {
|
|||||||
for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done
|
for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done
|
||||||
done
|
done
|
||||||
# The same rules as the menu's toggles.
|
# The same rules as the menu's toggles.
|
||||||
[[ "${WANTED[single]:-0}" == 1 ]] && WANTED[gaming]=1
|
# (Single user mode only needs the conversion where it is offered: not on NVIDIA PCs.)
|
||||||
|
[[ "${WANTED[single]:-0}" == 1 ]] && component_available gaming && WANTED[gaming]=1
|
||||||
|
if component_available gaming; then
|
||||||
[[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; }
|
[[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; }
|
||||||
|
else
|
||||||
|
WANTED[boot]=0
|
||||||
|
fi
|
||||||
|
[[ "${WANTED[bigpicture]:-0}" == 1 ]] && WANTED[nvidia]=1
|
||||||
|
[[ "${WANTED[nvidia]:-0}" == 0 ]] && WANTED[bigpicture]=0
|
||||||
[[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1
|
[[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1
|
||||||
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0
|
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0
|
||||||
[[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1
|
[[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1
|
||||||
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0
|
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0
|
||||||
[[ "${WANTED[hdmi]:-0}" == 1 ]] && { WANTED[machine]=1; WANTED[kpin]=1; }
|
# Retired: only ever removed.
|
||||||
[[ "${WANTED[kpin]:-0}" == 0 ]] && WANTED[hdmi]=0
|
WANTED[hdmi]=0
|
||||||
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
|
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
|
||||||
|
|
||||||
REAPPLY=$reapply
|
REAPPLY=$reapply
|
||||||
plan_changes
|
plan_changes
|
||||||
backend_event plan "\"disable\":$(json_list "${TO_DISABLE[@]}"),\"enable\":$(json_list "${TO_ENABLE[@]}")"
|
backend_event plan "\"disable\":$(json_list "${TO_DISABLE[@]}"),\"enable\":$(json_list "${TO_ENABLE[@]}")"
|
||||||
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
||||||
|
feature_record_unticked
|
||||||
backend_event finished '"failed":[],"restart":false,"nothing":true'
|
backend_event finished '"failed":[],"restart":false,"nothing":true'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -189,42 +183,18 @@ backend_apply() {
|
|||||||
for c in "${TO_ENABLE[@]}"; do
|
for c in "${TO_ENABLE[@]}"; do
|
||||||
if backend_run_component "$c" enable; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
|
if backend_run_component "$c" enable; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
|
||||||
done
|
done
|
||||||
|
feature_record_unticked
|
||||||
|
os_version_refresh
|
||||||
[[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] && RESTART_FOR_LOGIN=true
|
[[ " ${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_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)"
|
||||||
}
|
}
|
||||||
|
|
||||||
backend_hdmi() {
|
|
||||||
# backend_hdmi options|try|reset|forget [args]: the app's HDMI screen. Events
|
|
||||||
# only (no "finished"): the app goes on from its own screen.
|
|
||||||
local what="$1" rc
|
|
||||||
shift
|
|
||||||
backend_sudo || return 1
|
|
||||||
case "$what" in
|
|
||||||
options)
|
|
||||||
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1
|
|
||||||
backend_event hdmi-options "\"outputs\":$(hdmi_options 2>/dev/null)" ;;
|
|
||||||
try)
|
|
||||||
hdmi_try "$@" >/dev/null 2>&1; rc=$?
|
|
||||||
backend_event hdmi-tried "\"hz\":$4,\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
|
|
||||||
reset)
|
|
||||||
hdmi_reset "$@" >/dev/null 2>&1
|
|
||||||
backend_event hdmi-reset ;;
|
|
||||||
forget)
|
|
||||||
[[ "${1:-}" =~ ^[0-9a-f]{20}$ ]] && hdmi_forget "$1" >/dev/null 2>&1; rc=$?
|
|
||||||
backend_event hdmi-forgotten "\"id\":$(json_str "${1:-}"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_main() {
|
backend_main() {
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
status) backend_status ;;
|
status) backend_status ;;
|
||||||
apply) shift; backend_apply "$@" ;;
|
apply) shift; backend_apply "$@" ;;
|
||||||
bios-prepare) backend_bios_prepare ;;
|
bios-prepare) backend_bios_prepare ;;
|
||||||
bios-flash) backend_bios_flash ;;
|
bios-flash) backend_bios_flash ;;
|
||||||
hdmi-options) backend_hdmi options ;;
|
|
||||||
hdmi-try) shift; backend_hdmi try "$@" ;;
|
|
||||||
hdmi-reset) shift; backend_hdmi reset "$@" ;;
|
|
||||||
hdmi-forget) shift; backend_hdmi forget "$@" ;;
|
|
||||||
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
|
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
+1
-13
@@ -23,19 +23,7 @@ boot_enable() {
|
|||||||
# the session into the base config; run it right away rather than rely
|
# 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
|
# on its path unit, which could still be pending when the login manager
|
||||||
# starts.
|
# starts.
|
||||||
sudo tee "$BOOT_UNIT" > /dev/null << EOF
|
service_file "$BOOT_UNIT_NAME" SESSION_SYNC="$SESSION_SYNC" | sudo tee "$BOOT_UNIT" > /dev/null
|
||||||
[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
|
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable "$BOOT_UNIT_NAME" || { err "Enabling $BOOT_UNIT_NAME failed."; return 1; }
|
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)."
|
ok "Boots into the desktop from the next boot on (Return to Gaming Mode still works)."
|
||||||
|
|||||||
+31
-20
@@ -26,6 +26,10 @@ CEC_DKMS_VER=1
|
|||||||
CEC_DKMS_SRC="/usr/src/$CEC_DKMS_NAME-$CEC_DKMS_VER"
|
CEC_DKMS_SRC="/usr/src/$CEC_DKMS_NAME-$CEC_DKMS_VER"
|
||||||
CEC_DRIVER_URL="https://raw.githubusercontent.com/evlaV/linux-integration/10c8c8800ccd3ae359203b4eefb6479f613b3b8e/drivers/media/cec/platform/cros-ec/cros-ec-cec.c"
|
CEC_DRIVER_URL="https://raw.githubusercontent.com/evlaV/linux-integration/10c8c8800ccd3ae359203b4eefb6479f613b3b8e/drivers/media/cec/platform/cros-ec/cros-ec-cec.c"
|
||||||
CEC_DRIVER_SHA256=e89fd4e87fceb32d713ffc59b64794779b3c2b3c012b0e2f2e9f0c40b42f4373
|
CEC_DRIVER_SHA256=e89fd4e87fceb32d713ffc59b64794779b3c2b3c012b0e2f2e9f0c40b42f4373
|
||||||
|
# Downloaded once, then kept: raw.githubusercontent.com rate-limits (HTTP 429) repeated downloads
|
||||||
|
# from one address, and turning CEC off and on again needs no network then. Named after its
|
||||||
|
# checksum, so a new pin (a new commit and checksum above) downloads its file once by itself.
|
||||||
|
CEC_DRIVER_CACHE="/var/cache/steamify/cros-ec-cec-${CEC_DRIVER_SHA256:0:12}.c"
|
||||||
|
|
||||||
cec_link_steamos_manager() {
|
cec_link_steamos_manager() {
|
||||||
# On a Steam Machine: steamos-manager writes cecd's config from Steam's
|
# On a Steam Machine: steamos-manager writes cecd's config from Steam's
|
||||||
@@ -34,11 +38,11 @@ cec_link_steamos_manager() {
|
|||||||
# called by Steam Machine support, which installs steamos-manager after
|
# called by Steam Machine support, which installs steamos-manager after
|
||||||
# this item ran.
|
# this item ran.
|
||||||
pacman -Q steamos-manager >/dev/null 2>&1 || return 0
|
pacman -Q steamos-manager >/dev/null 2>&1 || return 0
|
||||||
systemctl --user daemon-reload
|
user_systemctl daemon-reload
|
||||||
systemctl --user enable steamos-manager-configure-cecd.service 2>/dev/null
|
user_systemctl enable steamos-manager-configure-cecd.service 2>/dev/null
|
||||||
systemctl --user start steamos-manager-configure-cecd.service 2>/dev/null
|
user_systemctl start steamos-manager-configure-cecd.service 2>/dev/null
|
||||||
systemctl --user restart cecd.service 2>/dev/null
|
user_systemctl restart cecd.service 2>/dev/null
|
||||||
systemctl --user restart steamos-manager.service 2>/dev/null
|
user_systemctl restart steamos-manager.service 2>/dev/null
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +55,10 @@ cec_status() { cec_installed && [[ -f "$CEC_STEAM_DROPIN" ]] && cec_driver_ok; }
|
|||||||
|
|
||||||
cec_reload_driver() {
|
cec_reload_driver() {
|
||||||
# cecd keeps /dev/cec0 open, so the old module can't be unloaded under it.
|
# cecd keeps /dev/cec0 open, so the old module can't be unloaded under it.
|
||||||
systemctl --user stop cecd.service 2>/dev/null
|
user_systemctl stop cecd.service 2>/dev/null
|
||||||
sudo modprobe -r cros_ec_cec 2>/dev/null
|
sudo modprobe -r cros_ec_cec 2>/dev/null
|
||||||
sudo modprobe cros_ec_cec 2>/dev/null
|
sudo modprobe cros_ec_cec 2>/dev/null
|
||||||
systemctl --user start cecd.service 2>/dev/null
|
user_systemctl start cecd.service 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
cec_driver_enable() {
|
cec_driver_enable() {
|
||||||
@@ -65,21 +69,25 @@ cec_driver_enable() {
|
|||||||
fi
|
fi
|
||||||
install_kernel_headers || return 1
|
install_kernel_headers || return 1
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
|
if [[ -f "$CEC_DRIVER_CACHE" ]] && echo "$CEC_DRIVER_SHA256 $CEC_DRIVER_CACHE" | sha256sum -c --quiet - 2>/dev/null; then
|
||||||
|
cp "$CEC_DRIVER_CACHE" "$tmp/cros-ec-cec.c"
|
||||||
|
else
|
||||||
info "Downloading Valve's Steam Machine CEC driver..."
|
info "Downloading Valve's Steam Machine CEC driver..."
|
||||||
if ! curl -fsL "$CEC_DRIVER_URL" -o "$tmp/cros-ec-cec.c" ||
|
if ! curl -fsL --retry 2 "$CEC_DRIVER_URL" -o "$tmp/cros-ec-cec.c" ||
|
||||||
! echo "$CEC_DRIVER_SHA256 $tmp/cros-ec-cec.c" | sha256sum -c --quiet -; then
|
! echo "$CEC_DRIVER_SHA256 $tmp/cros-ec-cec.c" | sha256sum -c --quiet -; then
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
err "Downloading Valve's CEC driver failed (or its checksum didn't match)."
|
err "Downloading Valve's CEC driver failed (or its checksum didn't match)."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
# The name carries the checksum: a newer pin gets its own file, the old one goes.
|
||||||
|
sudo find "${CEC_DRIVER_CACHE%/*}" -maxdepth 1 -name 'cros-ec-cec-*.c' ! -name "${CEC_DRIVER_CACHE##*/}" -delete 2>/dev/null
|
||||||
|
sudo install -Dm644 "$tmp/cros-ec-cec.c" "$CEC_DRIVER_CACHE"
|
||||||
|
fi
|
||||||
# So it finds amdgpu's HDMI port (see the patch for why).
|
# So it finds amdgpu's HDMI port (see the patch for why).
|
||||||
patch_file cros-ec-cec-single-port.patch | patch -s -d "$tmp" -p1 ||
|
patch_file cros-ec-cec-single-port.patch | patch -s -d "$tmp" -p1 ||
|
||||||
{ rm -rf "$tmp"; err "Patching Valve's CEC driver failed."; return 1; }
|
{ rm -rf "$tmp"; err "Patching Valve's CEC driver failed."; return 1; }
|
||||||
echo 'obj-m += cros-ec-cec.o' >"$tmp/Makefile"
|
echo 'obj-m += cros-ec-cec.o' >"$tmp/Makefile"
|
||||||
printf '%s\n' "# Written by Steamify: Valve's cros_ec_cec, which knows the Steam Machine." \
|
patch_file "$CEC_DKMS_NAME.dkms.conf" | fill NAME="$CEC_DKMS_NAME" VERSION="$CEC_DKMS_VER" >"$tmp/dkms.conf"
|
||||||
"PACKAGE_NAME=\"$CEC_DKMS_NAME\"" "PACKAGE_VERSION=\"$CEC_DKMS_VER\"" \
|
|
||||||
'BUILT_MODULE_NAME[0]="cros-ec-cec"' 'DEST_MODULE_LOCATION[0]="/updates/dkms"' \
|
|
||||||
'AUTOINSTALL="yes"' >"$tmp/dkms.conf"
|
|
||||||
sudo dkms remove "$CEC_DKMS_NAME/$CEC_DKMS_VER" --all >/dev/null 2>&1
|
sudo dkms remove "$CEC_DKMS_NAME/$CEC_DKMS_VER" --all >/dev/null 2>&1
|
||||||
sudo rm -rf "$CEC_DKMS_SRC"
|
sudo rm -rf "$CEC_DKMS_SRC"
|
||||||
sudo install -d "$CEC_DKMS_SRC" && sudo install -m644 "$tmp"/{cros-ec-cec.c,Makefile,dkms.conf} "$CEC_DKMS_SRC/"
|
sudo install -d "$CEC_DKMS_SRC" && sudo install -m644 "$tmp"/{cros-ec-cec.c,Makefile,dkms.conf} "$CEC_DKMS_SRC/"
|
||||||
@@ -157,10 +165,13 @@ cec_enable() {
|
|||||||
sudo tee "$CEC_STEAM_ENV" >/dev/null
|
sudo tee "$CEC_STEAM_ENV" >/dev/null
|
||||||
printf '%s\n' "# Written by Steamify: overrides STEAM_ENABLE_CEC=0 from the gaming mode script." \
|
printf '%s\n' "# Written by Steamify: overrides STEAM_ENABLE_CEC=0 from the gaming mode script." \
|
||||||
'[Service]' "EnvironmentFile=-$CEC_STEAM_ENV" | sudo tee "$CEC_STEAM_DROPIN" >/dev/null
|
'[Service]' "EnvironmentFile=-$CEC_STEAM_ENV" | sudo tee "$CEC_STEAM_DROPIN" >/dev/null
|
||||||
systemctl --user daemon-reload
|
user_systemctl daemon-reload
|
||||||
|
# The TV remote's volume keys: SteamOS enables this socket through its
|
||||||
|
# preset, which pacman doesn't apply, so nothing else does on CachyOS.
|
||||||
|
user_systemctl enable --now cec-audio-control.socket 2>/dev/null
|
||||||
cec_link_steamos_manager
|
cec_link_steamos_manager
|
||||||
if compgen -G "/dev/cec*" >/dev/null; then
|
if compgen -G "/dev/cec*" >/dev/null; then
|
||||||
systemctl --user restart cecd.service 2>/dev/null
|
user_systemctl restart cecd.service 2>/dev/null
|
||||||
ok "HDMI-CEC on ($(cd /dev && echo cec*)); Steam shows its settings from the next gaming mode start."
|
ok "HDMI-CEC on ($(cd /dev && echo cec*)); Steam shows its settings from the next gaming mode start."
|
||||||
info "Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)."
|
info "Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)."
|
||||||
else
|
else
|
||||||
@@ -171,20 +182,20 @@ cec_enable() {
|
|||||||
|
|
||||||
cec_disable() {
|
cec_disable() {
|
||||||
info "Removing HDMI-CEC..."
|
info "Removing HDMI-CEC..."
|
||||||
systemctl --user disable --now cecd.service cec-audio-control.socket cec-audio-control.service 2>/dev/null
|
user_systemctl disable --now cecd.service cec-audio-control.socket cec-audio-control.service 2>/dev/null
|
||||||
systemctl --user disable steamos-manager-configure-cecd.service 2>/dev/null
|
user_systemctl disable steamos-manager-configure-cecd.service 2>/dev/null
|
||||||
sudo pacman -Rns --noconfirm "${CEC_PKGS[@]}" 2>/dev/null
|
sudo pacman -Rns --noconfirm "${CEC_PKGS[@]}" 2>/dev/null
|
||||||
cec_driver_disable
|
cec_driver_disable
|
||||||
sudo rm -f "$CEC_ORDER_DROPIN" "$CEC_STEAM_DROPIN" "$CEC_STEAM_ENV"
|
sudo rm -f "$CEC_ORDER_DROPIN" "$CEC_STEAM_DROPIN" "$CEC_STEAM_ENV"
|
||||||
sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" "$(dirname "$CEC_STEAM_DROPIN")" "$(dirname "$CEC_STEAM_ENV")" 2>/dev/null
|
sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" "$(dirname "$CEC_STEAM_DROPIN")" "$(dirname "$CEC_STEAM_ENV")" 2>/dev/null
|
||||||
systemctl --user daemon-reload
|
user_systemctl daemon-reload
|
||||||
if [[ -n "$(state_get cec installed_linuxconsole)" ]]; then
|
if [[ -n "$(state_get cec installed_linuxconsole)" ]]; then
|
||||||
sudo pacman -Rns --noconfirm linuxconsole 2>/dev/null
|
sudo pacman -Rns --noconfirm linuxconsole 2>/dev/null
|
||||||
state_clear cec
|
state_clear cec
|
||||||
fi
|
fi
|
||||||
sudo udevadm control --reload
|
sudo udevadm control --reload
|
||||||
systemctl --user is-active -q steamos-manager.service 2>/dev/null &&
|
user_systemctl is-active -q steamos-manager.service 2>/dev/null &&
|
||||||
systemctl --user restart steamos-manager.service
|
user_systemctl restart steamos-manager.service
|
||||||
ok "HDMI-CEC removed."
|
ok "HDMI-CEC removed."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,5 +203,5 @@ cec_overview() {
|
|||||||
# For the menu's kernel overview.
|
# For the menu's kernel overview.
|
||||||
local dev="none"
|
local dev="none"
|
||||||
compgen -G "/dev/cec*" >/dev/null && dev="$(cd /dev && echo cec*)"
|
compgen -G "/dev/cec*" >/dev/null && dev="$(cd /dev && echo cec*)"
|
||||||
echo "CEC devices: $dev cecd: $(systemctl --user is-active cecd 2>/dev/null)"
|
echo "CEC devices: $dev cecd: $(user_systemctl is-active cecd 2>/dev/null)"
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,21 @@ ok() { echo -e "${c_green}[OK]${c_reset} $*"; }
|
|||||||
# patch_file <name>: a file from patches/ (module sources, patches). The
|
# patch_file <name>: a file from patches/ (module sources, patches). The
|
||||||
# single-file build replaces this with one that has them embedded.
|
# single-file build replaces this with one that has them embedded.
|
||||||
patch_file() { cat "$SCRIPT_DIR/patches/$1"; }
|
patch_file() { cat "$SCRIPT_DIR/patches/$1"; }
|
||||||
|
# service_file <name> [KEY=value...]: a systemd unit from services/, filled
|
||||||
|
# in. The single-file build replaces service_raw like patch_file.
|
||||||
|
service_raw() { cat "$SCRIPT_DIR/services/$1"; }
|
||||||
|
service_file() { local name="$1"; shift; service_raw "$name" | fill "$@"; }
|
||||||
|
# fill [KEY=value...]: stdin with each @KEY@ replaced by its value, taken
|
||||||
|
# literally (a home folder may contain sed's "&", "\" or "|").
|
||||||
|
fill() {
|
||||||
|
local kv v
|
||||||
|
local -a subst=(-e '')
|
||||||
|
for kv in "$@"; do
|
||||||
|
v="${kv#*=}"; v="${v//\\/\\\\}"; v="${v//&/\\&}"; v="${v//|/\\|}"
|
||||||
|
subst+=(-e "s|@${kv%%=*}@|$v|g")
|
||||||
|
done
|
||||||
|
sed "${subst[@]}"
|
||||||
|
}
|
||||||
warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
|
warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
|
||||||
err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; }
|
err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; }
|
||||||
ask_yn() {
|
ask_yn() {
|
||||||
@@ -29,6 +44,30 @@ require_root_helper() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user_session() {
|
||||||
|
# A running user systemd and session bus. Not in the installer's chroot,
|
||||||
|
# where the ISO applies Steamify for a user who has never logged in.
|
||||||
|
[[ -S "${XDG_RUNTIME_DIR:-/nonexistent}/bus" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_systemctl() {
|
||||||
|
# systemctl --user, also without a session: then only the unit files
|
||||||
|
# change (enable/disable through --root); starting and stopping is left
|
||||||
|
# to the first login, which starts what's enabled anyway.
|
||||||
|
if user_session; then systemctl --user "$@"; return; fi
|
||||||
|
local a verb="" args=()
|
||||||
|
for a in "$@"; do
|
||||||
|
[[ "$a" == --now ]] && continue
|
||||||
|
[[ -z "$verb" && "$a" != -* ]] && verb="$a"
|
||||||
|
args+=("$a")
|
||||||
|
done
|
||||||
|
case "$verb" in
|
||||||
|
enable|disable|is-enabled|mask|unmask) systemctl --user --root=/ "${args[@]}" ;;
|
||||||
|
is-active) return 1 ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
plasma_applets() {
|
plasma_applets() {
|
||||||
# Print "containment:applet" for every applet of plugin $1 in the
|
# Print "containment:applet" for every applet of plugin $1 in the
|
||||||
# user's Plasma layout, for kwriteconfig6 --group paths.
|
# user's Plasma layout, for kwriteconfig6 --group paths.
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Extended controller support: the Xbox wireless dongle (xone, with its firmware) and Xbox controllers over Bluetooth (xpadneo).
|
||||||
|
# The kernel has no driver for the dongle, and without xpadneo a Bluetooth Xbox controller works as a plain gamepad (buttons and
|
||||||
|
# sticks) but misses rumble (trigger motors too), proper button mapping and the battery level. Both are DKMS modules from CachyOS's
|
||||||
|
# own repo, built for every installed kernel (so every kernel needs its headers). Sourced by steamify.sh; not meant to be run on
|
||||||
|
# its own.
|
||||||
|
|
||||||
|
controller_xone_pkg() {
|
||||||
|
# A PC with the AUR's xone-dkms-git already works: the repo's xone-dkms would conflict with it.
|
||||||
|
pacman -Q xone-dkms-git >/dev/null 2>&1 && echo xone-dkms-git || echo xone-dkms
|
||||||
|
}
|
||||||
|
|
||||||
|
controller_pkgs() { echo "xpadneo-dkms $(controller_xone_pkg) xone-dongle-firmware"; }
|
||||||
|
|
||||||
|
extended_controller_support_status() {
|
||||||
|
# shellcheck disable=SC2046 # a list of package names
|
||||||
|
pacman -Q $(controller_pkgs) >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
extended_controller_support_enable() {
|
||||||
|
local p prev missing=()
|
||||||
|
info "Installing extended controller support (Xbox wireless dongle, Xbox Bluetooth)..."
|
||||||
|
install_kernel_headers || return 1
|
||||||
|
for p in $(controller_pkgs); do pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p"); done
|
||||||
|
if ((${#missing[@]})); then
|
||||||
|
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
|
||||||
|
{ err "Installing ${missing[*]} failed."; return 1; }
|
||||||
|
# What a first, half-finished run installed stays on the list.
|
||||||
|
prev="$(state_get extended_controller_support installed_pkgs)"
|
||||||
|
state_set extended_controller_support installed_pkgs "$(echo "$prev ${missing[*]}" | xargs)"
|
||||||
|
fi
|
||||||
|
ok "Extended controller support on: plug the dongle in again (or reconnect the controller) to use it."
|
||||||
|
}
|
||||||
|
|
||||||
|
extended_controller_support_disable() {
|
||||||
|
local pkgs
|
||||||
|
pkgs="$(state_get extended_controller_support installed_pkgs)"
|
||||||
|
# Only what Steamify installed; kernel headers stay.
|
||||||
|
# shellcheck disable=SC2086 # a list of package names
|
||||||
|
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
|
||||||
|
state_clear extended_controller_support
|
||||||
|
ok "Extended controller support off."
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# The ISO applies Steamify in the installer, before the user ever logged
|
||||||
|
# in (steamify.sh --defaults without a session). What needs Plasma's first
|
||||||
|
# layout is finished at the first desktop login, by a one-time autostart
|
||||||
|
# that then opens the app, next to CachyOS Hello.
|
||||||
|
# Sourced by steamify.sh; not meant to be run on its own.
|
||||||
|
|
||||||
|
FIRST_LOGIN_ENTRY="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/steamify-first-login.desktop"
|
||||||
|
FIRST_LOGIN_SCRIPT="$STEAMIFY_BIN/first-login"
|
||||||
|
|
||||||
|
first_login_schedule() {
|
||||||
|
install_executable "$FIRST_LOGIN_SCRIPT" 755 << EOF2
|
||||||
|
#!/bin/bash
|
||||||
|
# Installed by Steamify CachyOS: runs once, at the first desktop login.
|
||||||
|
set -o pipefail
|
||||||
|
curl -fsSL --max-time 30 "$WIZARD_URL" | bash -s -- --first-login
|
||||||
|
rm -f "$FIRST_LOGIN_ENTRY" "\$0"
|
||||||
|
[[ -x "$WIZARD_APP_LAUNCHER" ]] && exec "$WIZARD_APP_LAUNCHER"
|
||||||
|
EOF2
|
||||||
|
printf '%s\n' '[Desktop Entry]' 'Type=Application' 'Name=Steamify first login' \
|
||||||
|
"Exec=$FIRST_LOGIN_SCRIPT" 'OnlyShowIn=KDE;' 'X-KDE-autostart-phase=2' 'NoDisplay=true' |
|
||||||
|
install_executable "$FIRST_LOGIN_ENTRY" 644
|
||||||
|
}
|
||||||
|
|
||||||
|
first_login_run() {
|
||||||
|
# plasmashell has just laid out the desktop from the look-and-feel
|
||||||
|
# package; wait for its config, then put single user's launcher
|
||||||
|
# settings on the new launcher.
|
||||||
|
local _
|
||||||
|
for _ in $(seq 30); do
|
||||||
|
[[ -n "$(plasma_applets org.kde.plasma.kickoff)" ]] && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if single_status; then
|
||||||
|
stop_plasmashell_for_edit
|
||||||
|
single_launcher
|
||||||
|
restart_plasmashell_if_stopped
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -28,10 +28,7 @@ poweroff_fix_enable() {
|
|||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
patch_file "$POWEROFF_DKMS_NAME.c" > "$tmp/$POWEROFF_DKMS_NAME.c"
|
patch_file "$POWEROFF_DKMS_NAME.c" > "$tmp/$POWEROFF_DKMS_NAME.c"
|
||||||
echo "obj-m += $POWEROFF_DKMS_NAME.o" >"$tmp/Makefile"
|
echo "obj-m += $POWEROFF_DKMS_NAME.o" >"$tmp/Makefile"
|
||||||
printf '%s\n' "# Written by Steamify: the Steam Machine powers off instead of booting up again." \
|
patch_file "$POWEROFF_DKMS_NAME.dkms.conf" | fill NAME="$POWEROFF_DKMS_NAME" VERSION="$POWEROFF_DKMS_VER" >"$tmp/dkms.conf"
|
||||||
"PACKAGE_NAME=\"$POWEROFF_DKMS_NAME\"" "PACKAGE_VERSION=\"$POWEROFF_DKMS_VER\"" \
|
|
||||||
"BUILT_MODULE_NAME[0]=\"$POWEROFF_DKMS_NAME\"" 'DEST_MODULE_LOCATION[0]="/updates/dkms"' \
|
|
||||||
'AUTOINSTALL="yes"' >"$tmp/dkms.conf"
|
|
||||||
sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1
|
sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1
|
||||||
sudo rm -rf "$POWEROFF_DKMS_SRC"
|
sudo rm -rf "$POWEROFF_DKMS_SRC"
|
||||||
sudo install -d "$POWEROFF_DKMS_SRC" &&
|
sudo install -d "$POWEROFF_DKMS_SRC" &&
|
||||||
|
|||||||
+26
-619
@@ -1,37 +1,22 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# "HDMI refresh boost" menu item, Steam Machine (Fremont) only: finds the
|
# "HDMI refresh boost" menu item (hdmi), retired in 2.2.0: it saved an EDID
|
||||||
# highest refresh rate the display runs at the desktop resolution over HDMI,
|
# override per display (with a hotplug script loading it) so the pinned
|
||||||
# tests it live and saves an EDID override per display, loaded only while
|
# 7.1.6 kernel ran HDMI displays above 60 Hz. Newer kernels read the whole
|
||||||
# that display is connected (see hdmi_install_hotplug). Two things keep displays below what they can do:
|
# EDID themselves, and the kernel pin is gone with the power-off fix, so
|
||||||
# - Monitors put their fast modes in an extra EDID block (HDMI Forum EEODB);
|
# only its removal is left: shown while anything of it is on the system,
|
||||||
# the pinned kernel (7.1.6) only reads the first extension. Newer kernels
|
# always unticked, so a normal run removes it.
|
||||||
# do, and do HDMI 2.1, so it's only offered with the pinned kernel.
|
# Sourced by steamify.sh; not meant to be run on its own.
|
||||||
# - Their fastest modes need HDMI 2.1 (FRL), which amdgpu doesn't do on
|
|
||||||
# every kernel; a mode with the display's shortest blanking at a slightly
|
|
||||||
# lower rate often still fits HDMI 2.0's TMDS limit (600 MHz).
|
|
||||||
# What a display claims isn't what it accepts: every step is shown and must
|
|
||||||
# be confirmed, and no answer within 15 s switches back (the screen may be
|
|
||||||
# black). Sourced by steamify.sh; not meant to be run on its own.
|
|
||||||
|
|
||||||
HDMI_FW_DIR=/usr/lib/firmware/edid
|
HDMI_FW_DIR=/usr/lib/firmware/edid
|
||||||
# Before 2.1.0 the override was on the kernel command line; only removed now.
|
# Before 2.1.0 the override was on the kernel command line.
|
||||||
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
||||||
# The saved displays, one per line: <id> TAB <name> TAB <w>x<h> TAB <rates>.
|
# The saved displays; their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
|
||||||
# Their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
|
|
||||||
HDMI_MAP=/etc/steamify/hdmi-edid.conf
|
HDMI_MAP=/etc/steamify/hdmi-edid.conf
|
||||||
# Per output, the ID of the display whose EDID is loaded, or "reset".
|
|
||||||
HDMI_RUN=/run/steamify-edid
|
HDMI_RUN=/run/steamify-edid
|
||||||
HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug
|
HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug
|
||||||
HDMI_UNIT_NAME=steamify-edid.service
|
HDMI_UNIT_NAME=steamify-edid.service
|
||||||
HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME"
|
HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME"
|
||||||
HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules
|
HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules
|
||||||
# No answer within this time switches back; longer for scripted tests.
|
|
||||||
HDMI_CONFIRM_SECONDS="${WIZARD_HDMI_CONFIRM_SECONDS:-15}"
|
|
||||||
# amdgpu's HDMI 2.0 limit; the display's own limit (from its EDID) may be lower.
|
|
||||||
HDMI_TMDS_MAX_KHZ=600000
|
|
||||||
# Rates picked and tested in the app ("<output>=<w>x<h>:<rate>,..."), set by
|
|
||||||
# the backend's apply --hdmi; empty in the terminal menu, which tests itself.
|
|
||||||
HDMI_CHOICE=()
|
|
||||||
|
|
||||||
hdmi_connectors() {
|
hdmi_connectors() {
|
||||||
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
||||||
@@ -55,307 +40,29 @@ hdmi_cmdline_param() {
|
|||||||
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
|
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_available() {
|
hdmi_available() { detect_valve_fremont && hdmi_status; }
|
||||||
# No longer offered (it needed the pinned kernel): only shown while
|
|
||||||
# anything of it is left, so a normal run removes it.
|
|
||||||
detect_valve_fremont && hdmi_status
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_status() {
|
hdmi_status() {
|
||||||
# On while anything of it is left: saved displays (connected or not), the
|
# On while anything of it is left: saved displays, the hotplug script,
|
||||||
# hotplug script, an EDID file, or the kernel parameter of versions
|
# an EDID file, or the kernel parameter of versions before 2.1.0.
|
||||||
# before 2.1.0.
|
[[ -s "$HDMI_MAP" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
|
||||||
[[ -n "$(hdmi_saved)" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
|
|
||||||
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
|
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_saved() { [[ -f "$HDMI_MAP" ]] && grep -v '^#' "$HDMI_MAP"; }
|
hdmi_reset_live() {
|
||||||
|
# Puts a connected display back on its own EDID right away. debugfs is
|
||||||
hdmi_active_ids() {
|
# root-only, so the glob runs under sudo; the kernel takes exactly
|
||||||
# IDs of the connected displays whose saved EDID is loaded.
|
# "reset", without a newline.
|
||||||
local f
|
|
||||||
for f in "$HDMI_RUN"/*; do [[ -f "$f" ]] && grep -vx reset "$f"; done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_edid_tool() {
|
|
||||||
# hdmi_edid_tool <command> ...: EDID parsing and building (python, as
|
|
||||||
# it's byte work). Commands:
|
|
||||||
# blocks <edid> number of blocks the display says it has
|
|
||||||
# id <edid> vendor/product/serial, to recognise it
|
|
||||||
# plan <edid> <w> <h> <cur_hz> "<limit_hz> <best> <step>..." for w x h
|
|
||||||
# build <in> <out> <w> <h> <hz>... full EDID plus a DisplayID block
|
|
||||||
# with those rates at the shortest blanking
|
|
||||||
python3 - "$HDMI_TMDS_MAX_KHZ" "$@" <<'PY'
|
|
||||||
import sys
|
|
||||||
|
|
||||||
src_khz = int(sys.argv[1]); cmd = sys.argv[2]; args = sys.argv[3:]
|
|
||||||
def load(p): return bytearray(open(p, 'rb').read())
|
|
||||||
def le(b): return int.from_bytes(b, 'little')
|
|
||||||
|
|
||||||
def cta_blocks(e):
|
|
||||||
for i in range(128, len(e), 128):
|
|
||||||
if e[i] == 0x02: yield i
|
|
||||||
|
|
||||||
def data_blocks(e, i):
|
|
||||||
# CTA data block collection: (tag, extended tag, offset, length)
|
|
||||||
end, p = e[i + 2], i + 4
|
|
||||||
while p < i + end and p < i + 127:
|
|
||||||
tag, n = e[p] >> 5, e[p] & 0x1f
|
|
||||||
yield tag, (e[p + 1] if tag == 7 and n else None), p, n
|
|
||||||
p += n + 1
|
|
||||||
|
|
||||||
def eeodb(e):
|
|
||||||
for i in cta_blocks(e):
|
|
||||||
for tag, ext, p, n in data_blocks(e, i):
|
|
||||||
if tag == 7 and ext == 0x78: return p + 2
|
|
||||||
return None
|
|
||||||
|
|
||||||
def nblocks(e):
|
|
||||||
p = eeodb(e)
|
|
||||||
return 1 + max(e[126], e[p] if p and p < len(e) else 0)
|
|
||||||
|
|
||||||
def timings(e):
|
|
||||||
# (hz, clk_khz, ha, hb, hf, hs, hpos, va, vb, vf, vs, vpos)
|
|
||||||
out = []
|
|
||||||
def dtd(b):
|
|
||||||
clk = le(b[0:2]) * 10
|
|
||||||
if not clk: return
|
|
||||||
ha = b[2] | (b[4] >> 4) << 8; hb = b[3] | (b[4] & 15) << 8
|
|
||||||
va = b[5] | (b[7] >> 4) << 8; vb = b[6] | (b[7] & 15) << 8
|
|
||||||
hf = b[8] | (b[11] >> 6 & 3) << 8; hs = b[9] | (b[11] >> 4 & 3) << 8
|
|
||||||
vf = (b[10] >> 4) | (b[11] >> 2 & 3) << 4; vs = (b[10] & 15) | (b[11] & 3) << 4
|
|
||||||
if b[17] & 0x80: return # interlaced
|
|
||||||
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, hf, hs, b[17] >> 1 & 1,
|
|
||||||
va, vb, vf, vs, b[17] >> 2 & 1))
|
|
||||||
for o in range(54, 126, 18): dtd(e[o:o + 18])
|
|
||||||
for i in cta_blocks(e):
|
|
||||||
d = e[i + 2]
|
|
||||||
if d >= 4:
|
|
||||||
for o in range(i + d, i + 127 - 17, 18): dtd(e[o:o + 18])
|
|
||||||
for i in range(128, len(e), 128):
|
|
||||||
if e[i] != 0x70: continue
|
|
||||||
p, end = i + 5, i + 5 + e[i + 2]
|
|
||||||
while p + 3 <= end:
|
|
||||||
tag, n = e[p], e[p + 2]
|
|
||||||
if tag == 0 and n == 0: break
|
|
||||||
if tag == 0x03:
|
|
||||||
for o in range(p + 3, p + 3 + n - 19, 20):
|
|
||||||
b = e[o:o + 20]
|
|
||||||
clk = (le(b[0:3]) + 1) * 10
|
|
||||||
hf, vf = le(b[8:10]), le(b[16:18])
|
|
||||||
ha, hb, va, vb = le(b[4:6]) + 1, le(b[6:8]) + 1, le(b[12:14]) + 1, le(b[14:16]) + 1
|
|
||||||
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, (hf & 0x7fff) + 1,
|
|
||||||
le(b[10:12]) + 1, hf >> 15, va, vb, (vf & 0x7fff) + 1, le(b[18:20]) + 1, vf >> 15))
|
|
||||||
p += 3 + n
|
|
||||||
return out
|
|
||||||
|
|
||||||
def tmds_khz(e):
|
|
||||||
# The display's own TMDS limit: HDMI Forum VSDB, else the HDMI 1.4 VSDB.
|
|
||||||
hf = h14 = 0
|
|
||||||
for i in cta_blocks(e):
|
|
||||||
for tag, ext, p, n in data_blocks(e, i):
|
|
||||||
if tag != 3 or n < 3: continue
|
|
||||||
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
|
||||||
if oui == 0xC45DD8 and n >= 5: hf = e[p + 5] * 5000
|
|
||||||
if oui == 0x000C03 and n >= 7: h14 = e[p + 7] * 5000
|
|
||||||
lim = hf or h14 or 165000
|
|
||||||
return min(lim, src_khz)
|
|
||||||
|
|
||||||
def max_vrefresh(e):
|
|
||||||
# Range limits descriptor, the HDMI Forum / AMD VRR maximum.
|
|
||||||
best = 0
|
|
||||||
for o in range(54, 126, 18):
|
|
||||||
b = e[o:o + 18]
|
|
||||||
if b[0:3] == b'\0\0\0' and b[3] == 0xfd:
|
|
||||||
best = max(best, b[6] + (255 if b[4] & 2 else 0))
|
|
||||||
for i in cta_blocks(e):
|
|
||||||
for tag, ext, p, n in data_blocks(e, i):
|
|
||||||
if tag != 3 or n < 3: continue
|
|
||||||
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
|
||||||
if oui == 0xC45DD8 and n >= 10: best = max(best, (e[p + 9] & 0xc0) << 2 | e[p + 10])
|
|
||||||
if oui == 0x00001A and n >= 7: best = max(best, e[p + 7])
|
|
||||||
return best
|
|
||||||
|
|
||||||
def shortest(e, w, h):
|
|
||||||
ts = [t for t in timings(e) if t[2] == w and t[7] == h]
|
|
||||||
return min(ts, key=lambda t: (t[2] + t[3]) * (t[7] + t[8])) if ts else None
|
|
||||||
|
|
||||||
if cmd == 'blocks':
|
|
||||||
print(nblocks(load(args[0])))
|
|
||||||
elif cmd == 'id':
|
|
||||||
print(load(args[0])[8:18].hex())
|
|
||||||
elif cmd == 'name':
|
|
||||||
e = load(args[0])
|
|
||||||
for o in range(54, 126, 18):
|
|
||||||
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
|
|
||||||
print(e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip())
|
|
||||||
elif cmd == 'plan':
|
|
||||||
e = load(args[0]); w, h, cur = int(args[1]), int(args[2]), float(args[3])
|
|
||||||
lim, t = tmds_khz(e), shortest(e, w, h)
|
|
||||||
if not t: sys.exit(1)
|
|
||||||
listed = [x[0] for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
|
||||||
best = max(listed) if listed else cur
|
|
||||||
top = int(lim * 1000 // ((t[2] + t[3]) * (t[7] + t[8])))
|
|
||||||
vmax = max_vrefresh(e) or int(max(x[0] for x in timings(e) if x[2] == w and x[7] == h))
|
|
||||||
top = min(top, vmax)
|
|
||||||
# Not right at the limit: the top rounded down to ten, and the hundred
|
|
||||||
# below it as a safe option. Rates the display already lists are left
|
|
||||||
# out (its own timing is the safer one), and so is anything not faster
|
|
||||||
# than what already works.
|
|
||||||
steps = sorted({top // 10 * 10, top // 100 * 100})
|
|
||||||
steps = [s for s in steps if s > best + 1 and not any(abs(s - x) < 1 for x in listed)]
|
|
||||||
print(lim // 1000, round(best, 2), *steps)
|
|
||||||
elif cmd == 'options':
|
|
||||||
# JSON for the app: the display's name, the HDMI limit and the rates to
|
|
||||||
# offer at w x h: its own modes the kernel skips ("monitor"), calculated
|
|
||||||
# tens up to the limit ("calculated", ticked) and the exact limit when
|
|
||||||
# that's no ten ("limit", not ticked).
|
|
||||||
import json
|
|
||||||
e, k = load(args[0]), load(args[1]); w, h, cur = int(args[2]), int(args[3]), float(args[4])
|
|
||||||
lim, t = tmds_khz(e), shortest(e, w, h)
|
|
||||||
name = ''
|
|
||||||
for o in range(54, 126, 18):
|
|
||||||
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
|
|
||||||
name = e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip()
|
|
||||||
rates = []
|
|
||||||
if t:
|
|
||||||
fits = [x for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
|
||||||
known = [x[0] for x in timings(k) if x[2] == w and x[7] == h]
|
|
||||||
listed = [x[0] for x in fits]
|
|
||||||
best = max(listed + [cur])
|
|
||||||
for x in sorted(fits):
|
|
||||||
if x[0] > cur + 1 and not any(abs(x[0] - y) < 0.5 for y in known):
|
|
||||||
rates.append({'hz': round(x[0], 2), 'mhz': round(x[1] / 1000, 1), 'kind': 'monitor', 'pick': True})
|
|
||||||
size = (t[2] + t[3]) * (t[7] + t[8])
|
|
||||||
vmax = max_vrefresh(e) or int(max(listed + [cur]))
|
|
||||||
top = min(int(lim * 1000 // size), vmax)
|
|
||||||
tens = [r for r in range(10, top + 1, 10) if r > best + 1 and not any(abs(r - y) < 1 for y in listed)][-3:]
|
|
||||||
for r in tens:
|
|
||||||
rates.append({'hz': r, 'mhz': round(size * r / 1e6, 1), 'kind': 'calculated', 'pick': True})
|
|
||||||
if top % 10 and top > best + 1:
|
|
||||||
rates.append({'hz': top, 'mhz': round(size * top / 1e6, 1), 'kind': 'limit', 'pick': False})
|
|
||||||
print(json.dumps({'name': name, 'limit': lim // 1000, 'rates': rates}))
|
|
||||||
elif cmd == 'build':
|
|
||||||
e = load(args[0]); w, h = int(args[2]), int(args[3]); rates = [int(r) for r in args[4:]]
|
|
||||||
t = shortest(e, w, h)
|
|
||||||
n = nblocks(e)
|
|
||||||
e = e[:n * 128]
|
|
||||||
out = b''
|
|
||||||
for hz in rates:
|
|
||||||
_, _, ha, hb, hf, hs, hp, va, vb, vf, vs, vp = t
|
|
||||||
clk = round((ha + hb) * (va + vb) * hz / 1e4)
|
|
||||||
x = (clk - 1).to_bytes(3, 'little') + bytes([0x08])
|
|
||||||
for v, pol in ((ha, 0), (hb, 0), (hf, hp), (hs, 0), (va, 0), (vb, 0), (vf, vp), (vs, 0)):
|
|
||||||
x += ((v - 1) | pol << 15).to_bytes(2, 'little')
|
|
||||||
out += x
|
|
||||||
if out:
|
|
||||||
blk = bytearray(128)
|
|
||||||
blk[0:5] = bytes([0x70, 0x12, 121, 0x00, 0x00])
|
|
||||||
blk[5:8] = bytes([0x03, 0x00, len(out)])
|
|
||||||
blk[8:8 + len(out)] = out
|
|
||||||
blk[126] = (-sum(blk[1:126])) % 256
|
|
||||||
blk[127] = (-sum(blk[:127])) % 256
|
|
||||||
e += blk
|
|
||||||
count = len(e) // 128 - 1
|
|
||||||
e[126] = count
|
|
||||||
e[127] = (-sum(e[:127])) % 256
|
|
||||||
p = eeodb(e)
|
|
||||||
if p:
|
|
||||||
e[p] = count
|
|
||||||
i = p - p % 128
|
|
||||||
e[i + 127] = (-sum(e[i:i + 127])) % 256
|
|
||||||
open(args[1], 'wb').write(e)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_read_edid() {
|
|
||||||
# hdmi_read_edid <card-connector> <out>: the display's complete EDID,
|
|
||||||
# read over DDC block by block (the kernel's copy may miss blocks).
|
|
||||||
local conn="$1" out="$2" bus n i tmp
|
|
||||||
bus="$(basename "$(readlink -f "/sys/class/drm/$conn/ddc")")"; bus="${bus#i2c-}"
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
cat "/sys/class/drm/$conn/edid" > "$out"
|
|
||||||
if [[ -n "$bus" ]] && sudo modprobe i2c-dev 2>/dev/null; then
|
|
||||||
n="$(hdmi_edid_tool blocks "$out")"
|
|
||||||
: > "$tmp"
|
|
||||||
for (( i = 0; i < n; i++ )); do
|
|
||||||
# Segment pointer (0x30) selects each pair of blocks.
|
|
||||||
sudo i2ctransfer -y "$bus" w1@0x30 $(( i / 2 )) w1@0x50 $(( (i % 2) * 128 )) r128@0x50 2>/dev/null |
|
|
||||||
xxd -r -p >> "$tmp" 2>/dev/null ||
|
|
||||||
{ [[ $i -lt 2 ]] && { sudo i2ctransfer -y "$bus" w1@0x50 $(( i * 128 )) r128@0x50 | xxd -r -p >> "$tmp"; }; } || break
|
|
||||||
done
|
|
||||||
# Keep the DDC copy only when it's whole and the same display as the
|
|
||||||
# kernel's (not the whole block: a live override changes it).
|
|
||||||
if [[ "$(stat -c %s "$tmp")" == $(( n * 128 )) &&
|
|
||||||
"$(hdmi_edid_tool id "$tmp")" == "$(hdmi_edid_tool id "$out")" ]]; then
|
|
||||||
cat "$tmp" > "$out"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_debugfs() {
|
|
||||||
# debugfs directory of a connector (one of several paths to the same
|
|
||||||
# GPU). Only root can list debugfs, so the glob runs under sudo.
|
|
||||||
local d
|
local d
|
||||||
d="$(sudo sh -c 'for d in /sys/kernel/debug/dri/*/"$1"; do [ -e "$d/edid_override" ] && { echo "$d"; break; }; done' _ "${1#card*-}")"
|
d="$(sudo sh -c 'for d in /sys/kernel/debug/dri/*/"$1"; do [ -e "$d/edid_override" ] && { echo "$d"; break; }; done' _ "${1#card*-}")"
|
||||||
[[ -n "$d" ]] && echo "$d"
|
[[ -n "$d" ]] || return 1
|
||||||
}
|
printf reset | sudo tee "$d/edid_override" >/dev/null &&
|
||||||
|
|
||||||
hdmi_override_live() {
|
|
||||||
# hdmi_override_live <card-connector> <edid|reset>: swap the EDID the
|
|
||||||
# kernel uses right now, without a restart.
|
|
||||||
local d; d="$(hdmi_debugfs "$1")" || return 1
|
|
||||||
# The kernel takes exactly "reset", without a newline.
|
|
||||||
if [[ "$2" == reset ]]; then printf reset | sudo tee "$d/edid_override" >/dev/null
|
|
||||||
else sudo tee "$d/edid_override" < "$2" >/dev/null; fi || return 1
|
|
||||||
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
|
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
|
||||||
sleep 3
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_kscreen() {
|
|
||||||
# hdmi_kscreen current|mode <output> [<w> <h> <hz>]: KDE's view of the
|
|
||||||
# output: "w h hz" of the current mode, or set the mode closest to hz.
|
|
||||||
kscreen-doctor -j 2>/dev/null | python3 -c '
|
|
||||||
import json, sys
|
|
||||||
what, name = sys.argv[1], sys.argv[2]
|
|
||||||
for o in json.load(sys.stdin)["outputs"]:
|
|
||||||
if o["name"] != name: continue
|
|
||||||
modes = {m["id"]: m for m in o["modes"]}
|
|
||||||
if what == "current":
|
|
||||||
m = modes[o["currentModeId"]]
|
|
||||||
print(m["size"]["width"], m["size"]["height"], round(m["refreshRate"], 2))
|
|
||||||
else:
|
|
||||||
w, h, hz = int(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
|
||||||
ms = [m for m in o["modes"] if m["size"]["width"] == w and m["size"]["height"] == h]
|
|
||||||
if ms: print(min(ms, key=lambda m: abs(m["refreshRate"] - hz))["id"])
|
|
||||||
' "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_set_mode() {
|
|
||||||
# hdmi_set_mode <output> <w> <h> <hz>: returns 1 if KDE has no such mode.
|
|
||||||
local id rate
|
|
||||||
id="$(hdmi_kscreen mode "$@")" || return 1
|
|
||||||
[[ -n "$id" ]] || return 1
|
|
||||||
kscreen-doctor "output.$1.mode.$id" >/dev/null 2>&1 || return 1
|
|
||||||
sleep 2
|
|
||||||
rate="$(hdmi_kscreen current "$1" | cut -d' ' -f3)"
|
|
||||||
# Whole hertz: the rate asked for can be e.g. 59.97.
|
|
||||||
[[ "${rate%.*}" -ge $(( ${4%.*} - 1 )) ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_confirm() {
|
|
||||||
# No answer (black screen, nobody there) is a no.
|
|
||||||
local reply
|
|
||||||
read -rt "$HDMI_CONFIRM_SECONDS" -p "$(echo -e "${c_bold}$1${c_reset} [y/N, ${HDMI_CONFIRM_SECONDS}s] ")" reply || { echo; return 1; }
|
|
||||||
[[ "$reply" =~ ^[Yy]$ ]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_remove_boot_param() {
|
hdmi_remove_boot_param() {
|
||||||
# Removes the drm.edid_firmware= older versions put on the kernel command
|
# Removes the drm.edid_firmware= older versions put on the kernel command
|
||||||
# line (it applied to whatever display was on that port), then rebuilds
|
# line, then rebuilds the initramfs and boot entries.
|
||||||
# the initramfs and boot entries.
|
|
||||||
local f
|
local f
|
||||||
[[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; }
|
[[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; }
|
||||||
f="$(hdmi_boot_file)"
|
f="$(hdmi_boot_file)"
|
||||||
@@ -370,326 +77,26 @@ hdmi_remove_boot_param() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_write_map() {
|
hdmi_forget_all() {
|
||||||
# hdmi_write_map <lines>: the saved displays list.
|
# Every saved display and the hotplug script that loaded their EDIDs.
|
||||||
sudo mkdir -p "$(dirname "$HDMI_MAP")"
|
|
||||||
{ printf '# Written by steamify: HDMI refresh boost, one display per line:\n'
|
|
||||||
printf '# <id>\t<name>\t<w>x<h>\t<rates>\n'
|
|
||||||
[[ -n "$1" ]] && printf '%s\n' "$1"; } | sudo tee "$HDMI_MAP" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_save() {
|
|
||||||
# hdmi_save <edid> <w>x<h> <rates>: keep an EDID for the display it was
|
|
||||||
# built from (block 0 is the display's own, so is its ID), replacing
|
|
||||||
# what was saved for that display before.
|
|
||||||
local id name
|
|
||||||
id="$(hdmi_edid_tool id "$1")" && [[ -n "$id" ]] || return 1
|
|
||||||
name="$(hdmi_edid_tool name "$1")"
|
|
||||||
sudo install -Dm644 "$1" "$HDMI_FW_DIR/steamify-$id.bin" || return 1
|
|
||||||
hdmi_write_map "$(hdmi_saved | awk -F'\t' -v i="$id" '$1 != i'
|
|
||||||
printf '%s\t%s\t%s\t%s' "$id" "${name:-HDMI display}" "$2" "$3")"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_forget() {
|
|
||||||
# hdmi_forget <id>|all: remove saved displays; the hotplug script puts
|
|
||||||
# a connected one back on its own EDID, and goes when none are left.
|
|
||||||
local rest=""
|
|
||||||
if [[ "$1" == all ]]; then
|
|
||||||
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
|
|
||||||
else
|
|
||||||
rest="$(hdmi_saved | awk -F'\t' -v i="$1" '$1 != i')"
|
|
||||||
sudo rm -f "$HDMI_FW_DIR/steamify-$1.bin"
|
|
||||||
fi
|
|
||||||
if [[ -z "$rest" ]]; then hdmi_remove_hotplug; return; fi
|
|
||||||
hdmi_write_map "$rest"
|
|
||||||
sudo systemctl restart "$HDMI_UNIT_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_migrate() {
|
|
||||||
# Setups before 2.1.0: steamify-<output>.bin on the kernel command line,
|
|
||||||
# the display's ID and mode in the state file. Saved per display now.
|
|
||||||
local f out
|
|
||||||
local -a smode
|
|
||||||
for f in "$HDMI_FW_DIR"/steamify-*-*.bin; do
|
|
||||||
[[ -e "$f" ]] || continue
|
|
||||||
out="${f##*/steamify-}"; out="${out%.bin}"
|
|
||||||
read -ra smode <<< "$(state_get hdmi "$out-mode")"
|
|
||||||
[[ ${#smode[@]} -ge 2 ]] && hdmi_save "$f" "${smode[0]}x${smode[1]}" "${smode[*]:2}"
|
|
||||||
sudo rm -f "$f"
|
|
||||||
done
|
|
||||||
state_clear hdmi
|
|
||||||
hdmi_remove_boot_param
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_install_hotplug() {
|
|
||||||
# A saved EDID is only loaded while its display is connected: a udev
|
|
||||||
# rule runs the script at every hotplug (and a unit at boot, before the
|
|
||||||
# login manager). The script reads the display's ID over DDC, which
|
|
||||||
# shows the real display even while an override is loaded, and loads
|
|
||||||
# that display's file or resets to its own EDID. Unplugging resets too,
|
|
||||||
# so the next display never starts on the wrong one.
|
|
||||||
sudo tee "$HDMI_HOTPLUG" > /dev/null << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
# Steamify CachyOS, HDMI refresh boost: load a display's saved EDID only
|
|
||||||
# while it is connected. Runs as root (debugfs).
|
|
||||||
FW=/usr/lib/firmware/edid RUN=/run/steamify-edid
|
|
||||||
modprobe i2c-dev 2>/dev/null
|
|
||||||
mkdir -p "$RUN"
|
|
||||||
# Hotplug events come in bursts, and the link needs a moment before DDC works.
|
|
||||||
sleep 1
|
|
||||||
for c in /sys/class/drm/card*-HDMI-A-*; do
|
|
||||||
[ -e "$c" ] || continue
|
|
||||||
out="${c##*/}"; out="${out#card*-}"
|
|
||||||
new=reset id=""
|
|
||||||
if [ "$(cat "$c/status")" = connected ]; then
|
|
||||||
bus="$(basename "$(readlink -f "$c/ddc")")"
|
|
||||||
for i in 1 2 3 4 5; do
|
|
||||||
# Bytes 8-17 of block 0: manufacturer, product, serial, date.
|
|
||||||
id="$(i2ctransfer -y "${bus#i2c-}" w1@0x50 0 r18@0x50 2>/dev/null | tr -d ' ' | sed 's/0x//g')"
|
|
||||||
[ ${#id} -eq 36 ] && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
[ ${#id} -eq 36 ] && [ -f "$FW/steamify-${id:16}.bin" ] && new="${id:16}"
|
|
||||||
fi
|
|
||||||
[ "$(cat "$RUN/$out" 2>/dev/null || echo reset)" = "$new" ] && continue
|
|
||||||
for d in /sys/kernel/debug/dri/*/"$out"; do [ -e "$d/edid_override" ] && break; done
|
|
||||||
[ -e "$d/edid_override" ] || continue
|
|
||||||
if [ "$new" = reset ]; then printf reset > "$d/edid_override"
|
|
||||||
else cat "$FW/steamify-$new.bin" > "$d/edid_override"; fi
|
|
||||||
# Before the hotplug below, whose own event runs this again.
|
|
||||||
echo "$new" > "$RUN/$out"
|
|
||||||
echo 1 > "$d/trigger_hotplug"
|
|
||||||
done
|
|
||||||
exit 0
|
|
||||||
EOF
|
|
||||||
sudo chmod 755 "$HDMI_HOTPLUG"
|
|
||||||
# StartLimitIntervalSec=0: a burst of hotplugs must not get it rate-limited.
|
|
||||||
sudo tee "$HDMI_UNIT" > /dev/null << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Steamify CachyOS: HDMI refresh boost EDID for the connected display
|
|
||||||
StartLimitIntervalSec=0
|
|
||||||
Wants=sys-kernel-debug.mount
|
|
||||||
After=sys-kernel-debug.mount
|
|
||||||
Before=display-manager.service sddm.service plasmalogin.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=$HDMI_HOTPLUG
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical.target
|
|
||||||
EOF
|
|
||||||
# restart, not start: a hotplug during a run must still be looked at.
|
|
||||||
printf '%s\n' '# Written by steamify: HDMI refresh boost, check the display at every hotplug.' \
|
|
||||||
"ACTION==\"change\", SUBSYSTEM==\"drm\", ENV{HOTPLUG}==\"1\", RUN+=\"/usr/bin/systemctl --no-block restart $HDMI_UNIT_NAME\"" |
|
|
||||||
sudo tee "$HDMI_UDEV_RULE" >/dev/null || return 1
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo udevadm control --reload
|
|
||||||
sudo systemctl enable "$HDMI_UNIT_NAME" >/dev/null 2>&1 || { err "Enabling $HDMI_UNIT_NAME failed."; return 1; }
|
|
||||||
sudo systemctl restart "$HDMI_UNIT_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_remove_hotplug() {
|
|
||||||
local conn
|
local conn
|
||||||
|
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
|
||||||
sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1
|
sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1
|
||||||
sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP"
|
sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP"
|
||||||
sudo rm -rf "$HDMI_RUN"
|
sudo rm -rf "$HDMI_RUN"
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo udevadm control --reload
|
sudo udevadm control --reload
|
||||||
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done
|
for conn in $(hdmi_connectors); do hdmi_reset_live "$conn" 2>/dev/null; done
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_tune() {
|
|
||||||
# hdmi_tune <card-connector> <tmpdir>: find and confirm the highest rate
|
|
||||||
# at the desktop resolution. Sets HDMI_RESULT to "<w> <h> <hz>..." (the
|
|
||||||
# confirmed new rates) or leaves it empty.
|
|
||||||
local conn="$1" tmp="$2" out="${1#card*-}" cur w h hz plan lim best steps_str
|
|
||||||
local -a steps ok_rates=()
|
|
||||||
HDMI_RESULT=""
|
|
||||||
# From the display's own EDID: a live one left by an earlier test would
|
|
||||||
# make its rates look like what the display already does.
|
|
||||||
hdmi_override_live "$conn" reset 2>/dev/null
|
|
||||||
cur="$(hdmi_kscreen current "$out")"
|
|
||||||
[[ -n "$cur" ]] || { warn "$out: KDE doesn't show this output; skipped."; return 0; }
|
|
||||||
read -r w h hz <<< "$cur"
|
|
||||||
info "$out: desktop resolution ${w}x${h} at ${hz} Hz. Reading the display's EDID..."
|
|
||||||
hdmi_read_edid "$conn" "$tmp/$out.orig" || return 1
|
|
||||||
if ! plan="$(hdmi_edid_tool plan "$tmp/$out.orig" "$w" "$h" "$hz")"; then
|
|
||||||
warn "$out: the display lists no timing for ${w}x${h}; nothing to calculate."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
read -r lim best steps_str <<< "$plan"
|
|
||||||
read -ra steps <<< "${steps_str:-}"
|
|
||||||
info "$out: HDMI limit ${lim} MHz, fastest mode that fits now: ${best} Hz."
|
|
||||||
if [[ ${#steps[@]} -eq 0 ]]; then
|
|
||||||
ok "$out: already at the highest rate that fits HDMI here; nothing to change."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
info "$out: steps to try: ${steps[*]} Hz. After each one you're asked whether the"
|
|
||||||
info "picture is OK; no answer within ${HDMI_CONFIRM_SECONDS} s (e.g. a black screen) switches back."
|
|
||||||
ask_yn "Start the test?" y || return 0
|
|
||||||
|
|
||||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.test" "$w" "$h" "${steps[@]}" &&
|
|
||||||
hdmi_override_live "$conn" "$tmp/$out.test" ||
|
|
||||||
{ err "$out: couldn't load the test EDID (debugfs)."; return 1; }
|
|
||||||
local s
|
|
||||||
for s in "${steps[@]}"; do
|
|
||||||
info "$out: switching to ${w}x${h} at $s Hz..."
|
|
||||||
if hdmi_set_mode "$out" "$w" "$h" "$s" && hdmi_confirm "Is the picture OK at $s Hz?"; then
|
|
||||||
ok "$out: $s Hz works."
|
|
||||||
ok_rates+=("$s")
|
|
||||||
else
|
|
||||||
warn "$out: $s Hz doesn't work; stopping here."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ ${#ok_rates[@]} -eq 0 ]]; then
|
|
||||||
hdmi_override_live "$conn" reset
|
|
||||||
hdmi_set_mode "$out" "$w" "$h" "$hz" >/dev/null
|
|
||||||
info "$out: back to ${hz} Hz; nothing changed."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
# Without the steps that failed, so KDE and gamescope never pick them.
|
|
||||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${ok_rates[@]}" &&
|
|
||||||
hdmi_override_live "$conn" "$tmp/$out.final"
|
|
||||||
hdmi_set_mode "$out" "$w" "$h" "${ok_rates[-1]}" >/dev/null
|
|
||||||
HDMI_RESULT="$w $h ${ok_rates[*]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- For the app: options, live tries, and installing what was confirmed ---
|
|
||||||
# The EDIDs read by hdmi_options, reused by the tries and the install.
|
|
||||||
HDMI_CACHE="${XDG_RUNTIME_DIR:-/tmp}/steamify-hdmi"
|
|
||||||
|
|
||||||
hdmi_options() {
|
|
||||||
# JSON array: per connected HDMI output its name, mode and rate options.
|
|
||||||
local conn out cur w h hz opts sep=""
|
|
||||||
mkdir -p "$HDMI_CACHE"
|
|
||||||
printf '['
|
|
||||||
for conn in $(hdmi_connectors); do
|
|
||||||
out="${conn#card*-}"
|
|
||||||
# A live EDID left by an earlier try would look like the display's own.
|
|
||||||
hdmi_status || hdmi_override_live "$conn" reset 2>/dev/null
|
|
||||||
cur="$(hdmi_kscreen current "$out")"
|
|
||||||
[[ -n "$cur" ]] || continue
|
|
||||||
read -r w h hz <<< "$cur"
|
|
||||||
hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig" || continue
|
|
||||||
opts="$(hdmi_edid_tool options "$HDMI_CACHE/$out.orig" "/sys/class/drm/$conn/edid" "$w" "$h" "$hz")" || continue
|
|
||||||
# Every rate on offer loaded now, while the list is on screen: a try
|
|
||||||
# is then only a mode switch (the current mode stays in the EDID).
|
|
||||||
hdmi_status || hdmi_preload "$conn" "$w" "$h" "$opts"
|
|
||||||
printf '%s{"connector":"%s","output":"%s","width":%s,"height":%s,"hz":%s,"info":%s}' "$sep" "$conn" "$out" "$w" "$h" "$hz" "$opts"
|
|
||||||
sep=","
|
|
||||||
done
|
|
||||||
printf ']'
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_preload() {
|
|
||||||
# hdmi_preload <card-connector> <w> <h> <options json>: load the EDID
|
|
||||||
# with the calculated rates, as hdmi_try would on the first try.
|
|
||||||
local conn="$1" out="${1#card*-}"
|
|
||||||
local -a rates
|
|
||||||
read -ra rates <<< "$(python3 -c 'import json, sys
|
|
||||||
print(" ".join(str(r["hz"]) for r in json.loads(sys.argv[1])["rates"] if r["kind"] != "monitor"))' "$4")"
|
|
||||||
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$2" "$3" "${rates[@]}" &&
|
|
||||||
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" &&
|
|
||||||
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_try() {
|
|
||||||
# hdmi_try <card-connector> <w> <h> <hz> [<rate>...]: switch to hz, with
|
|
||||||
# an EDID holding the given calculated rates (loaded once per set).
|
|
||||||
local conn="$1" w="$2" h="$3" hz="$4" out="${1#card*-}"
|
|
||||||
shift 4
|
|
||||||
[[ -f "$HDMI_CACHE/$out.orig" ]] || hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig"
|
|
||||||
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$w" "$h" "$@" || return 1
|
|
||||||
if ! cmp -s "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded" 2>/dev/null; then
|
|
||||||
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" || return 1
|
|
||||||
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
|
||||||
fi
|
|
||||||
hdmi_set_mode "$out" "$w" "$h" "$hz"
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_reset() {
|
|
||||||
# hdmi_reset <card-connector> <w> <h> <hz>: the display's own EDID again.
|
|
||||||
hdmi_override_live "$1" reset
|
|
||||||
rm -f "$HDMI_CACHE/${1#card*-}.loaded"
|
|
||||||
hdmi_set_mode "${1#card*-}" "$2" "$3" "$4" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_choose() {
|
|
||||||
# For HDMI_CHOICE ("<output>=<w>x<h>:<rate>,..." from the app, rates it
|
|
||||||
# confirmed): builds the final EDIDs in <tmpdir> like hdmi_tune does.
|
|
||||||
local tmp="$1" item out mode rates w h
|
|
||||||
local -a r
|
|
||||||
for item in "${HDMI_CHOICE[@]}"; do
|
|
||||||
out="${item%%=*}"; mode="${item#*=}"; rates="${mode#*:}"; mode="${mode%%:*}"
|
|
||||||
w="${mode%x*}"; h="${mode#*x}"
|
|
||||||
IFS=',' read -ra r <<< "$rates"
|
|
||||||
HDMI_CHOSEN_CONN="$(basename /sys/class/drm/card*-"$out")"
|
|
||||||
if [[ -f "$HDMI_CACHE/$out.orig" ]]; then cp "$HDMI_CACHE/$out.orig" "$tmp/$out.orig"
|
|
||||||
else hdmi_read_edid "$HDMI_CHOSEN_CONN" "$tmp/$out.orig" || return 1; fi
|
|
||||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${r[@]}" &&
|
|
||||||
hdmi_override_live "$HDMI_CHOSEN_CONN" "$tmp/$out.final" || return 1
|
|
||||||
[[ ${#r[@]} -gt 0 ]] && hdmi_set_mode "$out" "$w" "$h" "${r[-1]}" >/dev/null
|
|
||||||
HDMI_RESULTS+=("$out|$w $h ${r[*]}")
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
hdmi_enable() {
|
hdmi_enable() {
|
||||||
local -a HDMI_RESULTS=()
|
err "HDMI refresh boost is no longer offered: current kernels run HDMI displays at their full rate."
|
||||||
if [[ -n "$(hdmi_cmdline_param)" ]]; then
|
|
||||||
info "Moving the EDID override off the kernel command line, so it only applies to its display..."
|
|
||||||
hdmi_migrate && hdmi_install_hotplug || { err "Moving it failed."; return 1; }
|
|
||||||
ok "HDMI refresh boost now only applies to the display it was set up for."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if hdmi_status; then
|
|
||||||
ok "Already set up for the connected display. Untick and tick it again to re-test."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ "${BACKEND:-false}" == true && ${#HDMI_CHOICE[@]} -eq 0 ]]; then
|
|
||||||
err "HDMI refresh boost needs you at the screen: pick and test the rates in the app first."
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
|
||||||
if [[ "${XDG_CURRENT_DESKTOP:-}" != *KDE* ]] || ! command -v kscreen-doctor >/dev/null; then
|
|
||||||
err "Run this from the Plasma desktop: the test switches the display mode through KDE."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1 || { err "Installing i2c-tools failed."; return 1; }
|
|
||||||
|
|
||||||
local tmp conn out res saved=false rc=0
|
|
||||||
local -a smode
|
|
||||||
tmp="$(mktemp -d)"
|
|
||||||
if [[ ${#HDMI_CHOICE[@]} -gt 0 ]]; then
|
|
||||||
# Picked and tested in the app.
|
|
||||||
hdmi_choose "$tmp" || rc=1
|
|
||||||
else
|
|
||||||
for conn in $(hdmi_connectors); do
|
|
||||||
hdmi_tune "$conn" "$tmp" || { rc=1; continue; }
|
|
||||||
[[ -n "$HDMI_RESULT" ]] && HDMI_RESULTS+=("${conn#card*-}|$HDMI_RESULT")
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
for res in "${HDMI_RESULTS[@]}"; do
|
|
||||||
out="${res%%|*}"; read -ra smode <<< "${res#*|}"
|
|
||||||
hdmi_save "$tmp/$out.final" "${smode[0]}x${smode[1]}" "${smode[*]:2}" && saved=true || rc=1
|
|
||||||
done
|
|
||||||
rm -rf "$tmp" "$HDMI_CACHE"
|
|
||||||
[[ "$saved" == true ]] || return $rc
|
|
||||||
if ! hdmi_install_hotplug; then
|
|
||||||
# Nothing half-done left behind; the live EDID lasts until a restart.
|
|
||||||
hdmi_remove_hotplug
|
|
||||||
err "Making it permanent failed; the display is back on its own EDID."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
ok "HDMI refresh boost is saved for this display; other displays keep their own settings."
|
|
||||||
return $rc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_disable() {
|
hdmi_disable() {
|
||||||
# Everything, for every saved display: newer kernels read the whole EDID
|
hdmi_forget_all
|
||||||
# themselves, and an old one loaded at the next hotplug would be wrong.
|
|
||||||
hdmi_forget all
|
|
||||||
hdmi_remove_boot_param || return 1
|
hdmi_remove_boot_param || return 1
|
||||||
state_clear hdmi
|
state_clear hdmi
|
||||||
ok "HDMI refresh boost removed; displays use their own EDID again."
|
ok "HDMI refresh boost removed; displays use their own EDID again."
|
||||||
|
|||||||
+93
-49
@@ -18,6 +18,78 @@ gaming_status() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# What Steam shows in its System settings besides the OS name (which stays
|
||||||
|
# CachyOS's, for legal clarity and because limine-snapper-sync finds the boot
|
||||||
|
# entries by it): the SteamOS release as the OS version, steam-machine as the
|
||||||
|
# codename, Steamify with its version as the variant (os-release VERSION_ID,
|
||||||
|
# VERSION_CODENAME, VARIANT_ID; see patches/steamify-os-release.sh).
|
||||||
|
OS_NAME_SCRIPT=/usr/local/libexec/steamify-os-release
|
||||||
|
OS_NAME_HOOK=/etc/pacman.d/hooks/zz-steamify-os-release.hook
|
||||||
|
# limine's tools take the OS name from TARGET_OS_NAME, else os-release's
|
||||||
|
# PRETTY_NAME: pinned to CachyOS, the boot entries never depend on os-release.
|
||||||
|
LIMINE_DEFAULTS=/etc/default/limine
|
||||||
|
|
||||||
|
steamos_release() {
|
||||||
|
# The SteamOS release Steamify follows: Valve's newest jupiter-X.Y repo
|
||||||
|
# (where the SteamOS extras come from), as an os-release ID (no spaces or
|
||||||
|
# capitals: steamos-3.9). Offline (e.g. in the installer): the one set
|
||||||
|
# before, if any.
|
||||||
|
local v
|
||||||
|
v="$(curl -fsL --max-time 15 "$VALVE_MIRROR/" 2>/dev/null | grep -oE 'jupiter-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)"
|
||||||
|
if [[ -n "$v" ]]; then echo "steamos-${v#jupiter-}"
|
||||||
|
else sed -n 's/^VERSION_ID=\(steamos-.*\)$/\1/p' /etc/os-release 2>/dev/null; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
os_name_enable() {
|
||||||
|
# 2.9.0 pre-releases renamed the OS itself; the name stays CachyOS's.
|
||||||
|
if grep -q '^DISTRIB_DESCRIPTION=.*with Steamify' /etc/lsb-release 2>/dev/null &&
|
||||||
|
[[ -x /usr/share/libalpm/scripts/cachyos-branding ]]; then
|
||||||
|
sudo /usr/share/libalpm/scripts/cachyos-branding lsb-release
|
||||||
|
fi
|
||||||
|
[[ -n "$(kreadconfig6 --file kcm-about-distrorc --group General --key Name 2>/dev/null)" ]] &&
|
||||||
|
kset gaming kcm-about-distrorc General Name --delete
|
||||||
|
# About this System: os-release's VERSION (CachyOS has none) instead of
|
||||||
|
# VERSION_ID, which is the SteamOS release now.
|
||||||
|
kset gaming kcm-about-distrorc General UseOSReleaseVersion true
|
||||||
|
patch_file steamify-os-release.sh | fill VERSION="$VERSION" STEAMOS="$(steamos_release)" |
|
||||||
|
sudo install -Dm755 /dev/stdin "$OS_NAME_SCRIPT" &&
|
||||||
|
patch_file steamify-os-release.hook | fill SCRIPT="$OS_NAME_SCRIPT" |
|
||||||
|
sudo install -Dm644 /dev/stdin "$OS_NAME_HOOK" &&
|
||||||
|
sudo "$OS_NAME_SCRIPT" || { err "Setting Steamify's version for Steam failed."; return 1; }
|
||||||
|
if [[ -f "$LIMINE_DEFAULTS" ]] && ! grep -q '^TARGET_OS_NAME=' "$LIMINE_DEFAULTS"; then
|
||||||
|
backup_file "$LIMINE_DEFAULTS"
|
||||||
|
printf '%s\n' 'TARGET_OS_NAME="CachyOS"' | sudo tee -a "$LIMINE_DEFAULTS" > /dev/null
|
||||||
|
state_set gaming limine_target 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
os_name_disable() {
|
||||||
|
sudo rm -f "$OS_NAME_HOOK" "$OS_NAME_SCRIPT"
|
||||||
|
[[ -f /etc/os-release ]] && sudo sed -i -e '/^VARIANT=/d' -e '/^VARIANT_ID=/d' -e '/^VERSION_ID=/d' -e '/^VERSION_CODENAME=/d' /etc/os-release
|
||||||
|
# CachyOS's own values back, the way its hooks write them.
|
||||||
|
if [[ -x /usr/share/libalpm/scripts/cachyos-branding ]]; then
|
||||||
|
sudo /usr/share/libalpm/scripts/cachyos-branding os-release
|
||||||
|
# Undoes 2.9.0 pre-releases, which changed lsb-release's name too.
|
||||||
|
sudo /usr/share/libalpm/scripts/cachyos-branding lsb-release
|
||||||
|
fi
|
||||||
|
if [[ -n "$(state_get gaming limine_target)" ]]; then
|
||||||
|
sudo sed -i '/^TARGET_OS_NAME="CachyOS"$/d' "$LIMINE_DEFAULTS"
|
||||||
|
state_set gaming limine_target ""
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
os_version_refresh() {
|
||||||
|
# After every run that changed something (sudo is at hand then): Steam's
|
||||||
|
# OS version follows Steamify's, not just the one that set it up. Silent:
|
||||||
|
# the app reads the backend's stdout as JSON.
|
||||||
|
gaming_status 2>/dev/null || return 0
|
||||||
|
grep -qx "VARIANT_ID=steamify-$VERSION" /etc/os-release 2>/dev/null && return 0
|
||||||
|
os_name_enable > /dev/null 2>&1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
gaming_enable() {
|
gaming_enable() {
|
||||||
# LOGIN_MANAGER (sddm with single user, else plasmalogin) is set by the
|
# LOGIN_MANAGER (sddm with single user, else plasmalogin) is set by the
|
||||||
# menu. Re-running with the other value switches over cleanly.
|
# menu. Re-running with the other value switches over cleanly.
|
||||||
@@ -37,6 +109,7 @@ gaming_enable() {
|
|||||||
esac
|
esac
|
||||||
create_desktop_shortcut
|
create_desktop_shortcut
|
||||||
steam_enable
|
steam_enable
|
||||||
|
os_name_enable || return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
gaming_disable() {
|
gaming_disable() {
|
||||||
@@ -49,6 +122,8 @@ gaming_disable() {
|
|||||||
switch_to_plasmalogin
|
switch_to_plasmalogin
|
||||||
remove_desktop_shortcut
|
remove_desktop_shortcut
|
||||||
steam_disable
|
steam_disable
|
||||||
|
krevert gaming
|
||||||
|
os_name_disable
|
||||||
ok "The PC boots to the normal login screen again (from the next boot)."
|
ok "The PC boots to the normal login screen again (from the next boot)."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +167,20 @@ switch_to_sddm() {
|
|||||||
ok "SDDM is the login manager from the next boot on."
|
ok "SDDM is the login manager from the next boot on."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove_sddm_base_autologin() {
|
||||||
|
# /etc/sddm.conf is read last: an [Autologin] there would override our fragments.
|
||||||
|
if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then
|
||||||
|
backup_file /etc/sddm.conf
|
||||||
|
sudo awk '
|
||||||
|
/^\[Autologin\]/ { skip=1; next }
|
||||||
|
/^\[/ { skip=0 }
|
||||||
|
!skip { print }
|
||||||
|
' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null &&
|
||||||
|
sudo mv /etc/sddm.conf.tmp /etc/sddm.conf
|
||||||
|
info "Removed [Autologin] from /etc/sddm.conf (backup kept)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
configure_sddm_autologin() {
|
configure_sddm_autologin() {
|
||||||
# User= and Relogin= live in our own fragment; steam-set-session keeps
|
# User= and Relogin= live in our own fragment; steam-set-session keeps
|
||||||
# Session= up to date in zz-steamos-autologin.conf, which sorts later
|
# Session= up to date in zz-steamos-autologin.conf, which sorts later
|
||||||
@@ -107,16 +196,7 @@ Session=gamescope-session.desktop
|
|||||||
Relogin=true
|
Relogin=true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then
|
remove_sddm_base_autologin
|
||||||
backup_file /etc/sddm.conf
|
|
||||||
sudo awk '
|
|
||||||
/^\[Autologin\]/ { skip=1; next }
|
|
||||||
/^\[/ { skip=0 }
|
|
||||||
!skip { print }
|
|
||||||
' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null &&
|
|
||||||
sudo mv /etc/sddm.conf.tmp /etc/sddm.conf
|
|
||||||
info "Removed [Autologin] from /etc/sddm.conf (backup kept)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start in gamescope, through CachyOS's own tool so its file is current.
|
# Start in gamescope, through CachyOS's own tool so its file is current.
|
||||||
sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop
|
sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop
|
||||||
@@ -185,24 +265,7 @@ install_session_sync() {
|
|||||||
local SYNC_SCRIPT="/usr/local/bin/sync-steamos-session.sh"
|
local SYNC_SCRIPT="/usr/local/bin/sync-steamos-session.sh"
|
||||||
info "Installing session sync bridge at $SYNC_SCRIPT"
|
info "Installing session sync bridge at $SYNC_SCRIPT"
|
||||||
|
|
||||||
sudo tee "$SYNC_SCRIPT" > /dev/null << 'EOF'
|
patch_file sync-steamos-session.sh | sudo tee "$SYNC_SCRIPT" > /dev/null
|
||||||
#!/bin/bash
|
|
||||||
SRC="/etc/plasmalogin.conf.d/zz-steamos-autologin.conf"
|
|
||||||
DEST="/etc/plasmalogin.conf"
|
|
||||||
|
|
||||||
[[ -f "$SRC" ]] || exit 0
|
|
||||||
SESSION=$(grep -oP '^Session=\K.*' "$SRC")
|
|
||||||
[[ -z "$SESSION" ]] && exit 0
|
|
||||||
|
|
||||||
# Only touch Session= inside [Autologin]; other sections may have their own.
|
|
||||||
if sed -n '/^\[Autologin\]/,/^\[/p' "$DEST" | grep -q '^Session='; then
|
|
||||||
sed -i "/^\[Autologin\]/,/^\[/ s|^Session=.*|Session=$SESSION|" "$DEST"
|
|
||||||
elif grep -q '^\[Autologin\]' "$DEST"; then
|
|
||||||
sed -i "/^\[Autologin\]/a Session=$SESSION" "$DEST"
|
|
||||||
else
|
|
||||||
printf '\n[Autologin]\nSession=%s\n' "$SESSION" >> "$DEST"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chmod +x "$SYNC_SCRIPT"
|
sudo chmod +x "$SYNC_SCRIPT"
|
||||||
ok "Sync script installed."
|
ok "Sync script installed."
|
||||||
@@ -211,28 +274,9 @@ EOF
|
|||||||
# 7. systemd path watcher + service
|
# 7. systemd path watcher + service
|
||||||
info "Installing systemd path watcher so session switches take effect immediately"
|
info "Installing systemd path watcher so session switches take effect immediately"
|
||||||
|
|
||||||
sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null << 'EOF'
|
service_file sync-steamos-session.path | sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null
|
||||||
[Unit]
|
|
||||||
Description=Watch for steamos session changes
|
|
||||||
|
|
||||||
[Path]
|
service_file sync-steamos-session.service | sudo tee /etc/systemd/system/sync-steamos-session.service > /dev/null
|
||||||
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
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now sync-steamos-session.path
|
sudo systemctl enable --now sync-steamos-session.path
|
||||||
|
|||||||
+157
-23
@@ -5,18 +5,19 @@
|
|||||||
|
|
||||||
# Menu order. Components are turned on in this order and off in reverse;
|
# Menu order. Components are turned on in this order and off in reverse;
|
||||||
# gaming must come first (single user builds on it).
|
# 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 nvidia bigpicture theme glyphs single launcher steamgame notify extended_controller_support vram cec machine poweroff kpin hdmi bios)
|
||||||
# One-off actions rather than on/off components: never preselected, never
|
# One-off actions rather than on/off components: never preselected, never
|
||||||
# re-applied, not listed as on or off.
|
# re-applied, not listed as on or off.
|
||||||
ACTIONS=(bios)
|
ACTIONS=(bios)
|
||||||
# Sub-options, shown indented under their parent and only while it's ticked.
|
# 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 [bigpicture]=nvidia [steamgame]=launcher [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine)
|
||||||
# Never preselected on a first run: booting into the desktop is a choice,
|
# 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
|
# 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,
|
# upset other devices on the TV, even on SteamOS), except on a Steam Machine,
|
||||||
# which has CEC like on SteamOS. HDMI refresh boost needs someone at the
|
# which has CEC like on SteamOS. HDMI refresh boost needs someone at the
|
||||||
# screen to confirm each step.
|
# screen to confirm each step. Extended controller support builds kernel modules for
|
||||||
NO_PRESELECT=(boot cec kpin hdmi)
|
# every installed kernel: opt-in.
|
||||||
|
NO_PRESELECT=(boot cec kpin hdmi extended_controller_support)
|
||||||
# Feature versions: the Steamify version in which what a component's enable
|
# Feature versions: the Steamify version in which what a component's enable
|
||||||
# sets up last changed; set it to the new VERSION whenever you change one.
|
# sets up last changed; set it to the new VERSION whenever you change one.
|
||||||
# Each successful run records it (state "features"); a component that's on
|
# Each successful run records it (state "features"); a component that's on
|
||||||
@@ -25,18 +26,23 @@ NO_PRESELECT=(boot cec kpin hdmi)
|
|||||||
# FEATURE_BASELINE.
|
# FEATURE_BASELINE.
|
||||||
FEATURE_BASELINE=2.1.0
|
FEATURE_BASELINE=2.1.0
|
||||||
declare -A FEATURE_VERSION=(
|
declare -A FEATURE_VERSION=(
|
||||||
[gaming]=2.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0
|
[gaming]=2.9.0 [boot]=2.1.0 [nvidia]=2.10.0 [bigpicture]=2.10.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.7.0 [machine]=2.9.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [extended_controller_support]=2.10.0
|
||||||
[kpin]=2.1.0 [hdmi]=2.1.0
|
[kpin]=2.1.0 [hdmi]=2.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
declare -A LABEL=(
|
declare -A LABEL=(
|
||||||
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
|
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
|
||||||
[boot]="Boot into the desktop instead of gaming mode"
|
[boot]="Boot into the desktop instead of gaming mode"
|
||||||
|
[nvidia]="Gaming on NVIDIA: Steam on the desktop, started at login"
|
||||||
|
[bigpicture]="Steam starts in Big Picture: the controller-friendly Steam (untick: Steam's normal window)"
|
||||||
[theme]="Install SteamOS theme: Vapor look (cachyos-vapor)"
|
[theme]="Install SteamOS theme: Vapor look (cachyos-vapor)"
|
||||||
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
|
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
|
||||||
[single]="Single user mode: no password, lock screen or log out (SDDM)"
|
[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"
|
[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"
|
||||||
|
[extended_controller_support]="Extended controller support: Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)"
|
||||||
[vram]="VRAM booster: the game in front keeps its VRAM, background apps make room"
|
[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)"
|
[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"
|
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
|
||||||
@@ -49,8 +55,16 @@ declare -A CURRENT WANTED
|
|||||||
|
|
||||||
component_available() {
|
component_available() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
# gamescope's own session is broken on NVIDIA: the conversion is replaced by "Gaming on NVIDIA" there (shown
|
||||||
|
# anyway when it's already on, so it can be turned off).
|
||||||
|
gaming|boot|glyphs) ! nvidia_present || gaming_status ;;
|
||||||
|
nvidia) nvidia_available ;;
|
||||||
|
bigpicture) bigpicture_available ;;
|
||||||
machine|poweroff) machine_available ;;
|
machine|poweroff) machine_available ;;
|
||||||
vram) vram_available ;;
|
vram) vram_available ;;
|
||||||
|
# Steamify in the Steam library is for gaming mode's controller: not needed for Big Picture on the desktop (NVIDIA PCs;
|
||||||
|
# shown anyway when it's already on, so it can be turned off).
|
||||||
|
steamgame) steamgame_available && { ! nvidia_present || steamgame_status; } ;;
|
||||||
kpin) kpin_available ;;
|
kpin) kpin_available ;;
|
||||||
hdmi) hdmi_available ;;
|
hdmi) hdmi_available ;;
|
||||||
bios) bios_available ;;
|
bios) bios_available ;;
|
||||||
@@ -72,6 +86,21 @@ menu_visible() {
|
|||||||
[[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]]
|
[[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feature_record_unticked() {
|
||||||
|
# After a run the user confirmed: options shown but left unticked that
|
||||||
|
# have no record yet count as turned off, or the next run would offer a
|
||||||
|
# default one again as "new" (feature_new) and tick it: e.g. single user
|
||||||
|
# mode unticked on a first run came back ticked when anything else
|
||||||
|
# changed. Hidden sub-options weren't a choice, so they're left alone.
|
||||||
|
local c
|
||||||
|
for c in "${COMPONENTS[@]}"; do
|
||||||
|
is_action "$c" && continue
|
||||||
|
menu_visible "$c" || continue
|
||||||
|
[[ "${WANTED[$c]:-0}" == 0 && -z "$(state_get features "$c")" ]] && state_set features "$c" off
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
feature_record() {
|
feature_record() {
|
||||||
# feature_record <component> <enable|disable>: after a successful run.
|
# feature_record <component> <enable|disable>: after a successful run.
|
||||||
if [[ "$2" == enable ]]; then state_set features "$1" "${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}"
|
if [[ "$2" == enable ]]; then state_set features "$1" "${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}"
|
||||||
@@ -95,7 +124,7 @@ menu_label() {
|
|||||||
# last run.
|
# last run.
|
||||||
local l="${LABEL[$1]}" b=""
|
local l="${LABEL[$1]}" b=""
|
||||||
if feature_outdated "$1"; then b="${c_yellow}(update)${c_reset}"
|
if feature_outdated "$1"; then b="${c_yellow}(update)${c_reset}"
|
||||||
elif feature_new "$1"; then b="${c_green}(new)${c_reset}"; fi
|
elif feature_new "$1" || feature_new_optin "$1"; then b="${c_green}(new)${c_reset}"; fi
|
||||||
if [[ -n "$b" ]]; then
|
if [[ -n "$b" ]]; then
|
||||||
if [[ "$l" == *:* ]]; then l="${l%%:*} $b:${l#*:}"
|
if [[ "$l" == *:* ]]; then l="${l%%:*} $b:${l#*:}"
|
||||||
else l+=" $b"; fi
|
else l+=" $b"; fi
|
||||||
@@ -103,16 +132,30 @@ menu_label() {
|
|||||||
printf '%s' "$l"
|
printf '%s' "$l"
|
||||||
}
|
}
|
||||||
|
|
||||||
feature_new() {
|
feature_added() {
|
||||||
# A default option added after its parent was set up (e.g. the power-off
|
# An option added after its parent was set up (e.g. the power-off fix under
|
||||||
# fix under Steam Machine support; a top-level one counts the SteamOS
|
# Steam Machine support; a top-level one counts the SteamOS conversion, or
|
||||||
# conversion as its parent): never turned on or off.
|
# "Gaming on NVIDIA" where that replaces it, as its parent), not on yet,
|
||||||
local p="${PARENT[$1]:-gaming}"
|
# with no record: shown as "new".
|
||||||
[[ "$1" != gaming && "${CURRENT[$p]:-0}" == 1 && "${CURRENT[$1]:-0}" == 0 ]] &&
|
local p="${PARENT[$1]:-}" parent_on=0
|
||||||
! is_action "$1" && component_selectable "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]] &&
|
if [[ -n "$p" ]]; then parent_on="${CURRENT[$p]:-0}"
|
||||||
|
elif [[ "${CURRENT[gaming]:-0}" == 1 || "${CURRENT[nvidia]:-0}" == 1 ]]; then parent_on=1; fi
|
||||||
|
[[ "$1" != gaming && "$parent_on" == 1 && "${CURRENT[$1]:-0}" == 0 ]] &&
|
||||||
|
! is_action "$1" && component_selectable "$1" &&
|
||||||
[[ -z "$(state_get features "$1")" ]]
|
[[ -z "$(state_get features "$1")" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feature_new() {
|
||||||
|
# A new default option: ticked for you (never turned on or off by itself).
|
||||||
|
feature_added "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
feature_new_optin() {
|
||||||
|
# A new opt-in option (unticked by default): the "new" badge only, it stays
|
||||||
|
# unticked. Boot into is a choice row of the conversion, not an item.
|
||||||
|
feature_added "$1" && [[ "$1" != boot && " ${NO_PRESELECT[*]} " == *" $1 "* ]]
|
||||||
|
}
|
||||||
|
|
||||||
component_selectable() {
|
component_selectable() {
|
||||||
# Greyed out and not tickable when it has nothing to do.
|
# Greyed out and not tickable when it has nothing to do.
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -123,6 +166,7 @@ component_selectable() {
|
|||||||
|
|
||||||
detect_components() {
|
detect_components() {
|
||||||
local c any=false
|
local c any=false
|
||||||
|
for c in "${COMPONENTS[@]}"; do CURRENT[$c]=0; WANTED[$c]=0; done
|
||||||
for c in "${COMPONENTS[@]}"; do
|
for c in "${COMPONENTS[@]}"; do
|
||||||
component_available "$c" || continue
|
component_available "$c" || continue
|
||||||
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
|
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
|
||||||
@@ -146,8 +190,11 @@ detect_components() {
|
|||||||
launcher_repair && WANTED[launcher]=1
|
launcher_repair && WANTED[launcher]=1
|
||||||
# Shows the current and newest BIOS version.
|
# Shows the current and newest BIOS version.
|
||||||
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
|
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
|
||||||
# Greyed out with an NVIDIA card: say why.
|
# Greyed out with an NVIDIA card: say why, and what to do.
|
||||||
component_available vram && ! vram_selectable && LABEL[vram]="VRAM booster: not supported by NVIDIA's driver yet"
|
if component_available vram && ! vram_selectable; then
|
||||||
|
VRAM_NVIDIA_CASE="$(vram_nvidia_case)"
|
||||||
|
LABEL[vram]="VRAM booster: $(vram_nvidia_hint "$VRAM_NVIDIA_CASE")"
|
||||||
|
fi
|
||||||
# First run: preselect the full SteamOS experience (never an action).
|
# First run: preselect the full SteamOS experience (never an action).
|
||||||
if [[ "$any" == false ]]; then
|
if [[ "$any" == false ]]; then
|
||||||
for c in "${COMPONENTS[@]}"; do
|
for c in "${COMPONENTS[@]}"; do
|
||||||
@@ -158,26 +205,111 @@ detect_components() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaults_options() {
|
||||||
|
# defaults_options [--options <id>[,<id>...]] [--boot gamescope|desktop]:
|
||||||
|
# what --defaults sets up, from the Steam Machine ISO's installer page.
|
||||||
|
# --options: exactly these items on, every other off (without it, the
|
||||||
|
# menu's preselection). An item that's on brings its parent (single user
|
||||||
|
# the conversion, like ticking it in the menu); one this PC can't use is
|
||||||
|
# left off with a warning. --boot desktop needs the conversion. Returns 1
|
||||||
|
# on an unknown option, item or value.
|
||||||
|
local c ids list=false boot=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
# Both take a value: without one, shift 2 fails and never shifts, so
|
||||||
|
# the loop would spin forever.
|
||||||
|
[[ "$1" == --options || "$1" == --boot ]] && [[ $# -lt 2 ]] && { err "$1 needs a value"; return 1; }
|
||||||
|
case "$1" in
|
||||||
|
--options)
|
||||||
|
list=true
|
||||||
|
IFS=, read -ra ids <<< "$2"
|
||||||
|
shift 2 ;;
|
||||||
|
--boot)
|
||||||
|
case "$2" in
|
||||||
|
gamescope|desktop) boot="$2" ;;
|
||||||
|
*) err "--boot takes gamescope or desktop"; return 1 ;;
|
||||||
|
esac
|
||||||
|
shift 2 ;;
|
||||||
|
*) err "Unknown option: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ "$list" == true ]]; then
|
||||||
|
for c in "${ids[@]}"; do
|
||||||
|
[[ -n "$c" ]] || continue
|
||||||
|
[[ -n "${LABEL[$c]:-}" ]] || { err "Unknown item: $c"; return 1; }
|
||||||
|
[[ "$c" == boot ]] && { err "Where to start is --boot, not an item."; return 1; }
|
||||||
|
is_action "$c" && { err "$c can't be part of the install."; return 1; }
|
||||||
|
done
|
||||||
|
for c in "${COMPONENTS[@]}"; do WANTED[$c]=0; done
|
||||||
|
for c in "${ids[@]}"; do
|
||||||
|
[[ -n "$c" ]] || continue
|
||||||
|
if component_available "$c" && component_selectable "$c"; then
|
||||||
|
WANTED[$c]=1
|
||||||
|
else
|
||||||
|
warn "Leaving out ${LABEL[$c]%%:*}: not available on this PC."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "${WANTED[single]}" == 1 ]] && component_available gaming && WANTED[gaming]=1
|
||||||
|
for c in "${COMPONENTS[@]}"; do
|
||||||
|
[[ "${WANTED[$c]}" == 1 && -n "${PARENT[$c]:-}" ]] && WANTED[${PARENT[$c]}]=1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
case "$boot" in
|
||||||
|
desktop)
|
||||||
|
[[ "${WANTED[gaming]}" == 1 ]] || { err "--boot desktop needs the SteamOS conversion (gaming)."; return 1; }
|
||||||
|
WANTED[boot]=1 ;;
|
||||||
|
gamescope) WANTED[boot]=0 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults_list() {
|
||||||
|
# --defaults --list: what --defaults can set up on this PC, as one JSON
|
||||||
|
# array, for an installer page (the Steam Machine ISO builds its Steamify
|
||||||
|
# page from it, so new items show up without a new ISO). Per item: id,
|
||||||
|
# label, hint, kind (toggle, or choice for boot: on = desktop), parent,
|
||||||
|
# on (preselected like a first run) and selectable. Actions, retired
|
||||||
|
# items and items this PC can't use (e.g. no Steam account yet) are
|
||||||
|
# left out. Read-only: no sudo, no state.
|
||||||
|
local c kind on sel items=""
|
||||||
|
for c in "${COMPONENTS[@]}"; do
|
||||||
|
component_available "$c" || continue
|
||||||
|
is_action "$c" && continue
|
||||||
|
[[ "$c" == kpin || "$c" == hdmi ]] && continue
|
||||||
|
kind=toggle; [[ "$c" == boot ]] && kind=choice
|
||||||
|
sel=false; component_selectable "$c" && sel=true
|
||||||
|
on=false
|
||||||
|
[[ "$sel" == true && " ${NO_PRESELECT[*]} " != *" $c "* ]] && on=true
|
||||||
|
[[ "$c" == cec ]] && machine_available && on=true
|
||||||
|
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[$c]:-}"),\"on\":$on,\"selectable\":$sel}"
|
||||||
|
done
|
||||||
|
printf '[%s]\n' "$items"
|
||||||
|
}
|
||||||
|
|
||||||
toggle_component() {
|
toggle_component() {
|
||||||
local c="$1"
|
local c="$1"
|
||||||
component_selectable "$c" || return 1
|
component_selectable "$c" || return 1
|
||||||
WANTED[$c]=$(( 1 - WANTED[$c] ))
|
WANTED[$c]=$(( 1 - WANTED[$c] ))
|
||||||
# Single user mode only makes sense on top of the SteamOS conversion.
|
# Single user mode only makes sense on top of the SteamOS conversion.
|
||||||
if [[ "$c" == single && "${WANTED[single]}" == 1 ]]; then WANTED[gaming]=1; fi
|
# (On NVIDIA the conversion isn't offered: single user mode logs in by itself, see lib/single-user.sh.)
|
||||||
|
if [[ "$c" == single && "${WANTED[single]}" == 1 ]] && component_available gaming; then WANTED[gaming]=1; fi
|
||||||
if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi
|
if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi
|
||||||
# Where to boot to is part of the conversion, too.
|
# Where to boot to is part of the conversion, too.
|
||||||
if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi
|
if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi
|
||||||
|
# Big Picture is opt-out: ticked along with "Gaming on NVIDIA".
|
||||||
|
if [[ "$c" == nvidia ]]; then WANTED[bigpicture]=${WANTED[nvidia]}; fi
|
||||||
|
if [[ "$c" == bigpicture && "${WANTED[bigpicture]}" == 1 ]]; then WANTED[nvidia]=1; fi
|
||||||
# The power-off fix is opt-out: ticked along with Steam Machine support.
|
# The power-off fix is opt-out: ticked along with Steam Machine support.
|
||||||
if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi
|
if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi
|
||||||
if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi
|
if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi
|
||||||
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; fi
|
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" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi
|
||||||
# HDMI refresh boost and the BIOS update sit under Steam Machine support;
|
|
||||||
# the boost also needs the pinned kernel (newer kernels don't need it).
|
|
||||||
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[hdmi]=0; WANTED[bios]=0; fi
|
|
||||||
if [[ "$c" == kpin && "${WANTED[kpin]}" == 0 ]]; then WANTED[hdmi]=0; fi
|
|
||||||
if [[ "$c" == hdmi && "${WANTED[hdmi]}" == 1 ]]; then WANTED[machine]=1; WANTED[kpin]=1; fi
|
|
||||||
if [[ "$c" == bios && "${WANTED[bios]}" == 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() {
|
show_menu() {
|
||||||
@@ -362,5 +494,7 @@ apply_changes() {
|
|||||||
else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi
|
else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi
|
||||||
if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
|
if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
|
||||||
done
|
done
|
||||||
|
feature_record_unticked
|
||||||
|
os_version_refresh
|
||||||
FAILED=("${failed[@]}")
|
FAILED=("${failed[@]}")
|
||||||
}
|
}
|
||||||
+249
@@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Gaming on NVIDIA PCs: the SteamOS conversion (gamescope session) isn't offered there, because gamescope's DRM session shows a
|
||||||
|
# corrupted picture on NVIDIA (NVIDIA's open bug 5240452: modes above 2560x1440@120). `nvidia` is the replacement: Steam on the
|
||||||
|
# Plasma desktop, installed when missing and started at login; `bigpicture` (a sub-option) makes it start in Big Picture.
|
||||||
|
# On a supported card (RTX 20 series or newer, `nvidia_supported`) it also sets up what NVIDIA's Wayland/DRM stack wants:
|
||||||
|
# nvidia-drm.modeset=1 and fbdev=1 on the kernel command line and the NVIDIA modules in the initramfs. That didn't cure
|
||||||
|
# gamescope's picture (the driver already reported both `Y`), but it is the standard setup and may be needed.
|
||||||
|
# Sourced by steamify.sh; not meant to be run on its own.
|
||||||
|
|
||||||
|
NVIDIA_UNIT=steamify-steam-autostart.service
|
||||||
|
NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1"
|
||||||
|
NVIDIA_INITRAMFS_CONF=${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf}
|
||||||
|
# The script and pacman hook that keep the initramfs file right at every kernel change.
|
||||||
|
NVIDIA_SCRIPT=${NVIDIA_SCRIPT:-/usr/local/libexec/steamify-nvidia-initramfs}
|
||||||
|
NVIDIA_HOOK=${NVIDIA_HOOK:-/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook}
|
||||||
|
# The running kernel's command line; the tests point this at a file.
|
||||||
|
NVIDIA_CMDLINE=${NVIDIA_CMDLINE:-/proc/cmdline}
|
||||||
|
# The DRM devices in sysfs; the tests point this at a fake tree.
|
||||||
|
NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm}
|
||||||
|
|
||||||
|
nvidia_unit_file() { echo "$HOME/.config/systemd/user/$NVIDIA_UNIT"; }
|
||||||
|
|
||||||
|
nvidia_present() {
|
||||||
|
# An NVIDIA GPU (PCI vendor 0x10de, display class) whose driver provides
|
||||||
|
# nvidia_drm. nouveau has none, and gamescope is not a problem there.
|
||||||
|
local d
|
||||||
|
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
|
||||||
|
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue
|
||||||
|
[[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
|
||||||
|
modinfo nvidia_drm >/dev/null 2>&1 && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shown on NVIDIA PCs, unless the SteamOS conversion is already on there (it
|
||||||
|
# has to be turned off first: both would start Steam at login).
|
||||||
|
nvidia_available() { nvidia_present && ! gaming_status 2>/dev/null; }
|
||||||
|
bigpicture_available() { nvidia_available; }
|
||||||
|
|
||||||
|
nvidia_status() {
|
||||||
|
pacman -Q steam >/dev/null 2>&1 && [[ -f "$(nvidia_unit_file)" ]] &&
|
||||||
|
user_systemctl is-enabled -q "$NVIDIA_UNIT" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_write_unit() {
|
||||||
|
# nvidia_write_unit [args]: the autostart unit, Steam started with those arguments.
|
||||||
|
local unit; unit="$(nvidia_unit_file)"
|
||||||
|
mkdir -p "$(dirname "$unit")"
|
||||||
|
service_file steamify-steam-autostart.service ARGS="$1" > "$unit" || { err "Writing $unit failed."; return 1; }
|
||||||
|
user_systemctl daemon-reload
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_enable() {
|
||||||
|
info "Gaming on NVIDIA: Steam on the desktop, started at login..."
|
||||||
|
if ! pacman -Q steam >/dev/null 2>&1; then
|
||||||
|
sudo pacman -S --needed --noconfirm steam || { err "Installing Steam failed."; return 1; }
|
||||||
|
state_set nvidia installed_pkgs steam
|
||||||
|
fi
|
||||||
|
# Keeps what the Big Picture option set up; a first run starts Steam normally.
|
||||||
|
local unit_args=""; bigpicture_status && unit_args=-gamepadui
|
||||||
|
nvidia_write_unit "$unit_args" || return 1
|
||||||
|
user_systemctl enable "$NVIDIA_UNIT" >/dev/null 2>&1 || { err "Enabling $NVIDIA_UNIT failed."; return 1; }
|
||||||
|
nvidia_kernel_enable || return 1
|
||||||
|
ok "Steam starts at your next login."
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_disable() {
|
||||||
|
local pkgs unit; unit="$(nvidia_unit_file)"
|
||||||
|
user_systemctl disable --now "$NVIDIA_UNIT" >/dev/null 2>&1
|
||||||
|
rm -f "$unit"
|
||||||
|
user_systemctl daemon-reload
|
||||||
|
pkgs="$(state_get nvidia installed_pkgs)"
|
||||||
|
# Only what Steamify installed; games and the Steam folder stay.
|
||||||
|
# shellcheck disable=SC2086 # a list of package names
|
||||||
|
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
|
||||||
|
state_clear nvidia
|
||||||
|
krevert bigpicture
|
||||||
|
nvidia_kernel_disable
|
||||||
|
ok "Steam no longer starts at login."
|
||||||
|
}
|
||||||
|
|
||||||
|
bigpicture_status() { grep -qs -- '-gamepadui' "$(nvidia_unit_file)"; }
|
||||||
|
|
||||||
|
bigpicture_enable() {
|
||||||
|
# The unit comes from nvidia_enable (it runs first); without it there is nothing to change.
|
||||||
|
[[ -f "$(nvidia_unit_file)" ]] || return 0
|
||||||
|
nvidia_write_unit -gamepadui || return 1
|
||||||
|
# Windows of the last session (Discord, a browser) would be restored on top of Big Picture.
|
||||||
|
kset bigpicture ksmserverrc General loginMode emptySession
|
||||||
|
ok "Steam starts in Big Picture at your next login, on an empty desktop."
|
||||||
|
}
|
||||||
|
|
||||||
|
bigpicture_disable() {
|
||||||
|
krevert bigpicture
|
||||||
|
[[ -f "$(nvidia_unit_file)" ]] || return 0
|
||||||
|
nvidia_write_unit "" && ok "Steam starts in its normal window at your next login."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Kernel parameters and early modules (a supported card only)
|
||||||
|
|
||||||
|
nvidia_supported() {
|
||||||
|
# A supported NVIDIA GPU: PCI vendor 0x10de, display class, its kernel
|
||||||
|
# driver installed, and not older than the RTX 20 series, decided like the
|
||||||
|
# VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists, every
|
||||||
|
# other card runs NVIDIA's open modules). Older cards are left alone.
|
||||||
|
local d
|
||||||
|
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
|
||||||
|
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue
|
||||||
|
[[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
|
||||||
|
vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" && continue
|
||||||
|
modinfo nvidia_drm >/dev/null 2>&1 && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_legacy_gpu() {
|
||||||
|
# 0 when the only NVIDIA GPU(s) are older than RTX 20 (reported, not fixed).
|
||||||
|
local d found=0
|
||||||
|
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
|
||||||
|
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
|
||||||
|
vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" || return 1
|
||||||
|
found=1
|
||||||
|
done
|
||||||
|
[[ $found == 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_boot_file() {
|
||||||
|
# Same boot loaders as the kernel command line elsewhere (lib/hdmi-refresh.sh).
|
||||||
|
hdmi_boot_file
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_param_set() {
|
||||||
|
# 0 when $1 (e.g. nvidia-drm.modeset=1) is on the kernel command line or
|
||||||
|
# already in the boot loader's file (not booted yet).
|
||||||
|
# The kernel treats "nvidia_drm" and "nvidia-drm" alike: CachyOS' own
|
||||||
|
# setup may already have written the underscore spelling.
|
||||||
|
local f re; f="$(nvidia_boot_file)"; re="${1//nvidia-drm/nvidia[-_]drm}"
|
||||||
|
grep -qwE -- "$re" "$NVIDIA_CMDLINE" 2>/dev/null ||
|
||||||
|
{ [[ -n "$f" ]] && grep -qwE -- "$re" "$f" 2>/dev/null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_params_missing() {
|
||||||
|
local p
|
||||||
|
for p in $NVIDIA_PARAMS; do nvidia_param_set "$p" || return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_initramfs_ok() {
|
||||||
|
[[ -f "$NVIDIA_INITRAMFS_CONF" ]] || grep -qE '^MODULES=.*nvidia_drm' /etc/mkinitcpio.conf 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_modules_everywhere() {
|
||||||
|
# 0 when every installed kernel has the nvidia_drm module (the script's own check).
|
||||||
|
patch_file steamify-nvidia-initramfs.sh | NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" bash -s check
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_hook_install() {
|
||||||
|
# Never installs an empty script or hook (a piped patch_file that failed
|
||||||
|
# still lets install succeed with no input).
|
||||||
|
local script hook
|
||||||
|
script="$(patch_file steamify-nvidia-initramfs.sh)" && [[ -n "$script" ]] &&
|
||||||
|
hook="$(patch_file steamify-nvidia-initramfs.hook | fill SCRIPT="$NVIDIA_SCRIPT")" && [[ -n "$hook" ]] || return 1
|
||||||
|
printf '%s\n' "$script" | sudo install -Dm755 /dev/stdin "$NVIDIA_SCRIPT" &&
|
||||||
|
printf '%s\n' "$hook" | sudo install -Dm644 /dev/stdin "$NVIDIA_HOOK"
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_rebuild_boot() {
|
||||||
|
local f; f="$(nvidia_boot_file)"
|
||||||
|
info "Rebuilding the initramfs and boot entries..."
|
||||||
|
case "$f" in
|
||||||
|
/etc/default/limine) sudo limine-mkinitcpio ;;
|
||||||
|
/etc/sdboot-manage.conf) sudo /usr/bin/mkinitcpio -P && sudo sdboot-manage gen ;;
|
||||||
|
/etc/default/grub) sudo /usr/bin/mkinitcpio -P && sudo grub-mkconfig -o /boot/grub/grub.cfg ;;
|
||||||
|
*) sudo /usr/bin/mkinitcpio -P ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_boot_has_params() {
|
||||||
|
# 0 when the generated boot entries carry the parameters (or can't be
|
||||||
|
# checked): limine.conf, systemd-boot's entries, grub.cfg.
|
||||||
|
local target
|
||||||
|
case "$1" in
|
||||||
|
*/limine) target="${NVIDIA_LIMINE_CONF:-/boot/limine.conf}" ;;
|
||||||
|
*/sdboot-manage.conf) target="${NVIDIA_SDBOOT_DIR:-/boot/loader/entries}" ;;
|
||||||
|
*/grub) target="${NVIDIA_GRUB_CFG:-/boot/grub/grub.cfg}" ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
sudo test -e "$target" || return 0
|
||||||
|
sudo grep -rqs 'nvidia-drm.modeset=1' "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_kernel_enable() {
|
||||||
|
nvidia_supported || return 0
|
||||||
|
local f changed=0 p; f="$(nvidia_boot_file)"
|
||||||
|
if nvidia_params_missing; then
|
||||||
|
if [[ -z "$f" ]]; then
|
||||||
|
warn "NVIDIA GPU found, but no supported boot loader: add '$NVIDIA_PARAMS' to the kernel command line yourself."
|
||||||
|
else
|
||||||
|
info "NVIDIA GPU found: adding $NVIDIA_PARAMS to the kernel command line (fixes a corrupted screen in gaming mode)..."
|
||||||
|
backup_file "$f"
|
||||||
|
case "$f" in
|
||||||
|
*/limine)
|
||||||
|
sudo sed -i -E "s/^(KERNEL_CMDLINE\[default\]\+?=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
|
||||||
|
*/sdboot-manage.conf)
|
||||||
|
sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
|
||||||
|
*/grub)
|
||||||
|
sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
|
||||||
|
esac
|
||||||
|
if ! grep -q "nvidia-drm.fbdev=1" "$f"; then
|
||||||
|
err "Couldn't add the NVIDIA parameters to $f (no kernel command line setting in it): add '$NVIDIA_PARAMS' yourself."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
changed=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
nvidia_hook_install || { err "Installing the NVIDIA initramfs hook failed."; return 1; }
|
||||||
|
local had=0; [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && had=1
|
||||||
|
sudo env NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" "$NVIDIA_SCRIPT" sync
|
||||||
|
if [[ -f "$NVIDIA_INITRAMFS_CONF" && $had == 0 ]]; then
|
||||||
|
info "Loading the NVIDIA modules early (initramfs)..."
|
||||||
|
changed=1
|
||||||
|
elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" && $had == 1 ]]; then
|
||||||
|
warn "A kernel without the NVIDIA modules is installed: not loading them early any more (rebuilding the initramfs without them)."
|
||||||
|
changed=1
|
||||||
|
elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && ! nvidia_initramfs_ok; then
|
||||||
|
warn "Not every installed kernel has the NVIDIA modules: not loading them early for now (the kernel parameters still apply; this is checked again at every kernel update)."
|
||||||
|
fi
|
||||||
|
if [[ $changed == 1 ]]; then
|
||||||
|
nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; }
|
||||||
|
if ! nvidia_boot_has_params "$f"; then
|
||||||
|
err "The boot entries don't have the NVIDIA parameters: the initramfs or boot entry build failed (see above)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok "NVIDIA gaming mode fix applied; it takes effect after a reboot."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_kernel_disable() {
|
||||||
|
local f changed=0; f="$(nvidia_boot_file)"
|
||||||
|
if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then
|
||||||
|
sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f"
|
||||||
|
changed=1
|
||||||
|
fi
|
||||||
|
[[ -f "$NVIDIA_INITRAMFS_CONF" ]] && { sudo rm -f "$NVIDIA_INITRAMFS_CONF"; changed=1; }
|
||||||
|
sudo rm -f "$NVIDIA_HOOK" "$NVIDIA_SCRIPT"
|
||||||
|
[[ $changed == 1 ]] && nvidia_rebuild_boot
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -101,7 +101,54 @@ single_launcher() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single_launcher_drop() {
|
||||||
|
# The theme's layout backup is taken when it's turned on: with single
|
||||||
|
# user on then, it holds single's launcher keys, and restoring it after
|
||||||
|
# single was turned off brings them back. Drop exactly single's values
|
||||||
|
# (a launcher set up differently by the user is left alone).
|
||||||
|
local applet
|
||||||
|
local -a kick_args
|
||||||
|
for applet in $(plasma_applets org.kde.plasma.kickoff); do
|
||||||
|
kick_args=(--file plasma-org.kde.plasma.desktop-appletsrc --group Containments --group "${applet%%:*}"
|
||||||
|
--group Applets --group "${applet#*:}" --group Configuration --group General)
|
||||||
|
[[ "$(kreadconfig6 "${kick_args[@]}" --key primaryActions)" == 3 ]] &&
|
||||||
|
kwriteconfig6 "${kick_args[@]}" --key primaryActions --delete
|
||||||
|
[[ "$(kreadconfig6 "${kick_args[@]}" --key systemFavorites)" == 'suspend,reboot,shutdown' ]] &&
|
||||||
|
kwriteconfig6 "${kick_args[@]}" --key systemFavorites --delete
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without the SteamOS conversion (NVIDIA PCs: gamescope's session is broken there) nothing logs in by itself, so single user
|
||||||
|
# mode does it: SDDM, autologin into the Plasma session. Named to sort after steam-set-session's zz-steamos-autologin.conf,
|
||||||
|
# which may still say gamescope from an earlier conversion.
|
||||||
|
SINGLE_AUTOLOGIN=${SINGLE_AUTOLOGIN:-/etc/sddm.conf.d/zzz-steamify-autologin.conf}
|
||||||
|
|
||||||
|
single_login_needed() { nvidia_present && ! gaming_status; }
|
||||||
|
|
||||||
|
single_login_enable() {
|
||||||
|
single_login_needed || return 0
|
||||||
|
switch_to_sddm || return 1
|
||||||
|
info "Logging in to the desktop by itself (SDDM autologin for $TARGET_USER)..."
|
||||||
|
sudo mkdir -p "$(dirname "$SINGLE_AUTOLOGIN")"
|
||||||
|
sudo tee "$SINGLE_AUTOLOGIN" > /dev/null << EOF
|
||||||
|
[Autologin]
|
||||||
|
User=$TARGET_USER
|
||||||
|
Session=plasma.desktop
|
||||||
|
Relogin=true
|
||||||
|
EOF
|
||||||
|
remove_sddm_base_autologin
|
||||||
|
}
|
||||||
|
|
||||||
|
single_login_disable() {
|
||||||
|
[[ -f "$SINGLE_AUTOLOGIN" ]] || return 0
|
||||||
|
sudo rm -f "$SINGLE_AUTOLOGIN"
|
||||||
|
# The way back is CachyOS's own login manager, unless the conversion is on (it keeps SDDM).
|
||||||
|
gaming_status || switch_to_plasmalogin
|
||||||
|
}
|
||||||
|
|
||||||
single_enable() {
|
single_enable() {
|
||||||
|
single_login_enable || return 1
|
||||||
info "Turning off the lock screen, user switching and logging out..."
|
info "Turning off the lock screen, user switching and logging out..."
|
||||||
stop_plasmashell_for_edit
|
stop_plasmashell_for_edit
|
||||||
|
|
||||||
@@ -127,6 +174,7 @@ single_enable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
single_disable() {
|
single_disable() {
|
||||||
|
single_login_disable
|
||||||
info "Restoring the lock screen, user switching and logging out..."
|
info "Restoring the lock screen, user switching and logging out..."
|
||||||
stop_plasmashell_for_edit
|
stop_plasmashell_for_edit
|
||||||
if has_journal single; then
|
if has_journal single; then
|
||||||
|
|||||||
+59
-1
@@ -5,7 +5,11 @@
|
|||||||
# deletes the key if it didn't exist before).
|
# deletes the key if it didn't exist before).
|
||||||
# Sourced by steamify.sh; not meant to be run on its own.
|
# Sourced by steamify.sh; not meant to be run on its own.
|
||||||
|
|
||||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/cachyos-gamescope-boot"
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/steamify"
|
||||||
|
# Steamify's own files in the home folder: the app (app/, from
|
||||||
|
# steamify-app.sh) and the shortcut's and notifier's scripts (bin/).
|
||||||
|
STEAMIFY_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/steamify"
|
||||||
|
STEAMIFY_BIN="$STEAMIFY_DATA/bin"
|
||||||
UNSET=$'\x01'
|
UNSET=$'\x01'
|
||||||
|
|
||||||
kset() {
|
kset() {
|
||||||
@@ -77,3 +81,57 @@ state_clear() {
|
|||||||
current_display_manager() {
|
current_display_manager() {
|
||||||
systemctl show -p Id --value display-manager 2>/dev/null | sed 's/\.service$//'
|
systemctl show -p Id --value display-manager 2>/dev/null | sed 's/\.service$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrate_dir() {
|
||||||
|
# migrate_dir <old> <new>: moves <old> to <new> and leaves a symlink.
|
||||||
|
# When both exist (an older release ran after a newer one), what <new>
|
||||||
|
# lacks is moved over; a file in both keeps <new>'s copy and <old> stays
|
||||||
|
# as it is. Returns 0 when something moved.
|
||||||
|
local old="$1" new="$2" f moved=1
|
||||||
|
[[ -d "$old" && ! -L "$old" ]] || return 1
|
||||||
|
if [[ ! -e "$new" ]]; then
|
||||||
|
mkdir -p "$(dirname "$new")"
|
||||||
|
mv "$old" "$new" && ln -s "$new" "$old"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
for f in "$old"/* "$old"/.[!.]*; do
|
||||||
|
[[ -e "$f" || -L "$f" ]] || continue
|
||||||
|
[[ -e "$new/${f##*/}" ]] && continue
|
||||||
|
mv "$f" "$new/" && moved=0
|
||||||
|
done
|
||||||
|
rmdir "$old" 2>/dev/null && ln -s "$new" "$old"
|
||||||
|
return "$moved"
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_layout() {
|
||||||
|
# Before 2.5.0 the state and scripts were under the project's old name
|
||||||
|
# (cachyos-gamescope-boot). Moved once; the old names stay as symlinks,
|
||||||
|
# so an older release that still runs finds the same state.
|
||||||
|
local old_state="${XDG_STATE_HOME:-$HOME/.local/state}/cachyos-gamescope-boot"
|
||||||
|
local old_bin="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot"
|
||||||
|
local old_icon="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/cachyos-gamescope-boot-wizard.svg"
|
||||||
|
local f
|
||||||
|
# Links left pointing at nothing once their component was turned off.
|
||||||
|
for f in "$old_state" "$old_bin"; do [[ -L "$f" && ! -e "$f" ]] && rm -f "$f"; done
|
||||||
|
migrate_dir "$old_state" "$STATE_DIR"
|
||||||
|
if migrate_dir "$old_bin" "$STEAMIFY_BIN"; then
|
||||||
|
# The notifier had the old paths built in; its unit and the
|
||||||
|
# shortcut's entries point at the scripts.
|
||||||
|
[[ -f "$STEAMIFY_BIN/steamify-notifier" ]] &&
|
||||||
|
patch_file steamify-notifier.py | install_executable "$STEAMIFY_BIN/steamify-notifier" 755
|
||||||
|
for f in "$NOTIFY_UNITS/$NOTIFY_NAME.service" "$WIZARD_APP_ENTRY" "$WIZARD_TERMINAL_ENTRY" \
|
||||||
|
"$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME"; do
|
||||||
|
[[ -f "$f" ]] && sed -i "s|$old_bin/|$STEAMIFY_BIN/|g" "$f"
|
||||||
|
done
|
||||||
|
user_systemctl daemon-reload 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [[ -f "$old_icon" ]]; then
|
||||||
|
if [[ -e "$WIZARD_ICON" ]]; then rm -f "$old_icon"; else mv "$old_icon" "$WIZARD_ICON"; fi
|
||||||
|
gtk-update-icon-cache -q -f -t "$(dirname "$(dirname "$(dirname "$WIZARD_ICON")")")" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
for f in "$WIZARD_APP_ENTRY" "$WIZARD_TERMINAL_ENTRY" "$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME"; do
|
||||||
|
[[ -f "$f" ]] && grep -q '^Icon=cachyos-gamescope-boot-wizard$' "$f" &&
|
||||||
|
sed -i 's|^Icon=cachyos-gamescope-boot-wizard$|Icon=steamify|' "$f"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+5
-21
@@ -16,40 +16,24 @@ steam_enable() {
|
|||||||
systemd_user_dir="$HOME/.config/systemd/user"
|
systemd_user_dir="$HOME/.config/systemd/user"
|
||||||
mkdir -p "$systemd_user_dir"
|
mkdir -p "$systemd_user_dir"
|
||||||
|
|
||||||
cat << 'EOF' > "$systemd_user_dir/steam-desktop-autostart.service"
|
service_file steam-desktop-autostart.service > "$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
|
|
||||||
|
|
||||||
# Clean out old .desktop shortcut so they don't fight
|
# Clean out old .desktop shortcut so they don't fight
|
||||||
rm -f "$HOME/.config/autostart/steam.desktop"
|
rm -f "$HOME/.config/autostart/steam.desktop"
|
||||||
|
|
||||||
# Reload user systemd context and activate the background loop
|
# Reload user systemd context and activate the background loop
|
||||||
systemctl --user daemon-reload
|
user_systemctl daemon-reload
|
||||||
systemctl --user enable --now steam-desktop-autostart.service
|
user_systemctl enable --now steam-desktop-autostart.service
|
||||||
|
|
||||||
ok "Steam UI parameters and keyboard autostart deployed successfully."
|
ok "Steam UI parameters and keyboard autostart deployed successfully."
|
||||||
}
|
}
|
||||||
|
|
||||||
steam_disable() {
|
steam_disable() {
|
||||||
info "Removing Steam desktop autostart..."
|
info "Removing Steam desktop autostart..."
|
||||||
systemctl --user disable --now steam-desktop-autostart.service 2>/dev/null
|
user_systemctl disable --now steam-desktop-autostart.service 2>/dev/null
|
||||||
rm -f "$HOME/.config/systemd/user/steam-desktop-autostart.service" \
|
rm -f "$HOME/.config/systemd/user/steam-desktop-autostart.service" \
|
||||||
"$HOME/.config/environment.d/99-kde-virtual-keyboard.conf"
|
"$HOME/.config/environment.d/99-kde-virtual-keyboard.conf"
|
||||||
systemctl --user daemon-reload
|
user_systemctl daemon-reload
|
||||||
ok "Steam desktop settings removed (takes full effect at next login)."
|
ok "Steam desktop settings removed (takes full effect at next login)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/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/<id>/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, "<account>": "<appid> added"
|
||||||
|
# or "<appid> 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 <function>: 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)..."
|
||||||
|
# A Steam that's still starting ignores the request: ask again.
|
||||||
|
for ((i = 0; i < 90; i++)); do
|
||||||
|
((i % 15 == 0)) && steam -shutdown >/dev/null 2>&1
|
||||||
|
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 user_systemctl is-enabled -q steam-desktop-autostart.service 2>/dev/null; then
|
||||||
|
user_systemctl 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."
|
||||||
|
}
|
||||||
+85
-38
@@ -167,9 +167,9 @@ kpin_enable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kpin_disable() {
|
kpin_disable() {
|
||||||
# Saved HDMI refresh boost EDIDs are for the pinned kernel only (newer
|
# Saved HDMI refresh boost EDIDs were for the pinned kernel only (newer
|
||||||
# ones read the whole EDID), also those of displays not connected now.
|
# ones read the whole EDID), also those of displays not connected now.
|
||||||
[[ -n "$(hdmi_saved)" ]] && hdmi_forget all
|
hdmi_status && hdmi_forget_all
|
||||||
remove_kernel_pin
|
remove_kernel_pin
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +224,11 @@ install_valve_led_driver() {
|
|||||||
# a clang-built tree). DKMS reads this override after the package's
|
# a clang-built tree). DKMS reads this override after the package's
|
||||||
# dkms.conf; written before the install so its own build works too.
|
# dkms.conf; written before the install so its own build works too.
|
||||||
# On a fresh system dkms isn't installed yet, so /etc/dkms doesn't exist.
|
# On a fresh system dkms isn't installed yet, so /etc/dkms doesn't exist.
|
||||||
|
# Someone's own override is backed up and kept, with ours after it: it's
|
||||||
|
# sourced as bash, so our MAKE[0] wins.
|
||||||
sudo mkdir -p "$(dirname "$LED_DKMS_OVERRIDE")"
|
sudo mkdir -p "$(dirname "$LED_DKMS_OVERRIDE")"
|
||||||
if ! printf '%s\n' "# Written by cachyos-gamescope-boot: build for DKMS's target kernel." \
|
if ! led_dkms_override | sudo tee "$LED_DKMS_OVERRIDE.new" >/dev/null ||
|
||||||
'MAKE[0]="make KVERSION=${kernelver}"' | sudo tee "$LED_DKMS_OVERRIDE" >/dev/null; then
|
! sudo mv -f "$LED_DKMS_OVERRIDE.new" "$LED_DKMS_OVERRIDE"; then
|
||||||
err "Couldn't write $LED_DKMS_OVERRIDE; without it the LED driver only builds for the running kernel."
|
err "Couldn't write $LED_DKMS_OVERRIDE; without it the LED driver only builds for the running kernel."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -285,7 +287,33 @@ install_valve_led_driver() {
|
|||||||
|
|
||||||
LED_UDEV_RULE="/etc/udev/rules.d/70-valve-leds-user.rules"
|
LED_UDEV_RULE="/etc/udev/rules.d/70-valve-leds-user.rules"
|
||||||
LED_DKMS_OVERRIDE="/etc/dkms/leds-valve-dkms.conf"
|
LED_DKMS_OVERRIDE="/etc/dkms/leds-valve-dkms.conf"
|
||||||
HEADERS_SCRIPT="/usr/local/lib/cachyos-gamescope-boot/ensure-kernel-headers"
|
|
||||||
|
led_dkms_override() {
|
||||||
|
# The override to write: an existing one's own lines (backed up the first
|
||||||
|
# time), without ours from an earlier run, then ours. Before 2.5.0 the
|
||||||
|
# file was ours alone, with a different comment.
|
||||||
|
local ours
|
||||||
|
ours="$(patch_file leds-valve-dkms.conf)"
|
||||||
|
if [[ -f "$LED_DKMS_OVERRIDE" ]]; then
|
||||||
|
grep -qxF -f <(echo "$ours") "$LED_DKMS_OVERRIDE" ||
|
||||||
|
grep -q '^# Written by cachyos-gamescope-boot' "$LED_DKMS_OVERRIDE" ||
|
||||||
|
backup_file "$LED_DKMS_OVERRIDE" >&2
|
||||||
|
grep -vxF -f <(echo "$ours") "$LED_DKMS_OVERRIDE" | grep -v '^# Written by cachyos-gamescope-boot'
|
||||||
|
fi
|
||||||
|
echo "$ours"
|
||||||
|
}
|
||||||
|
|
||||||
|
led_dkms_override_remove() {
|
||||||
|
# Puts someone's own override back, or removes ours.
|
||||||
|
if [[ -f "$LED_DKMS_OVERRIDE.bak-gamescope-wizard" ]]; then
|
||||||
|
sudo mv -f "$LED_DKMS_OVERRIDE.bak-gamescope-wizard" "$LED_DKMS_OVERRIDE"
|
||||||
|
else
|
||||||
|
sudo rm -f "$LED_DKMS_OVERRIDE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
HEADERS_SCRIPT="/usr/local/lib/steamify/ensure-kernel-headers"
|
||||||
|
# Before 2.5.0; removed when it's set up again or turned off.
|
||||||
|
HEADERS_SCRIPT_OLD="/usr/local/lib/cachyos-gamescope-boot/ensure-kernel-headers"
|
||||||
HEADERS_UNIT="/etc/systemd/system/ensure-kernel-headers.service"
|
HEADERS_UNIT="/etc/systemd/system/ensure-kernel-headers.service"
|
||||||
|
|
||||||
install_headers_boot_check() {
|
install_headers_boot_check() {
|
||||||
@@ -295,36 +323,10 @@ install_headers_boot_check() {
|
|||||||
# So check at every boot and install missing headers, which triggers
|
# So check at every boot and install missing headers, which triggers
|
||||||
# that hook and builds the driver for the new kernel.
|
# that hook and builds the driver for the new kernel.
|
||||||
sudo mkdir -p "$(dirname "$HEADERS_SCRIPT")"
|
sudo mkdir -p "$(dirname "$HEADERS_SCRIPT")"
|
||||||
sudo tee "$HEADERS_SCRIPT" > /dev/null << 'EOF'
|
patch_file ensure-kernel-headers.sh | sudo tee "$HEADERS_SCRIPT" > /dev/null
|
||||||
#!/bin/bash
|
|
||||||
# Installed by cachyos-gamescope-boot: install missing -headers for every
|
|
||||||
# installed kernel, so DKMS builds leds-valve for it.
|
|
||||||
set -u
|
|
||||||
missing=()
|
|
||||||
for k in $(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u); do
|
|
||||||
pacman -Q "${k}-headers" >/dev/null 2>&1 && continue
|
|
||||||
pacman -Si "${k}-headers" >/dev/null 2>&1 && missing+=("${k}-headers")
|
|
||||||
done
|
|
||||||
[[ ${#missing[@]} -eq 0 ]] && exit 0
|
|
||||||
# Another pacman is running (e.g. an update): try again next boot.
|
|
||||||
[[ -e /var/lib/pacman/db.lck ]] && { echo "pacman is busy, skipping"; exit 0; }
|
|
||||||
echo "Installing missing kernel headers: ${missing[*]}"
|
|
||||||
exec pacman -S --needed --noconfirm "${missing[@]}"
|
|
||||||
EOF
|
|
||||||
sudo chmod 755 "$HEADERS_SCRIPT"
|
sudo chmod 755 "$HEADERS_SCRIPT"
|
||||||
sudo tee "$HEADERS_UNIT" > /dev/null << EOF
|
sudo rm -rf "$(dirname "$HEADERS_SCRIPT_OLD")"
|
||||||
[Unit]
|
service_file ensure-kernel-headers.service HEADERS_SCRIPT="$HEADERS_SCRIPT" | sudo tee "$HEADERS_UNIT" > /dev/null
|
||||||
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
|
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable ensure-kernel-headers.service
|
sudo systemctl enable ensure-kernel-headers.service
|
||||||
}
|
}
|
||||||
@@ -376,6 +378,46 @@ machine_status() {
|
|||||||
pacman -Qi leds-valve-dkms-git >/dev/null 2>&1 && pacman -Qi steamos-manager >/dev/null 2>&1
|
pacman -Qi leds-valve-dkms-git >/dev/null 2>&1 && pacman -Qi steamos-manager >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Steam (steamwebhelper) reads the serial number for its System settings
|
||||||
|
# straight from DMI, which the kernel makes root-only; SteamOS makes it
|
||||||
|
# readable. A tmpfiles rule does that at every boot.
|
||||||
|
SERIAL_TMPFILES=/etc/tmpfiles.d/steamify-serial.conf
|
||||||
|
|
||||||
|
# Steam's Wi-Fi backend switch goes through steamos-manager, which reads and
|
||||||
|
# writes the backend in this file (SteamOS ships it); without it, it fails
|
||||||
|
# with "Wi-Fi backend not found in config". wpa_supplicant is what
|
||||||
|
# NetworkManager uses on CachyOS anyway. Not written when someone set a
|
||||||
|
# backend themselves.
|
||||||
|
WIFI_BACKEND_CONF=/etc/NetworkManager/conf.d/99-valve-wifi-backend.conf
|
||||||
|
|
||||||
|
wifi_backend_enable() {
|
||||||
|
[[ -f "$WIFI_BACKEND_CONF" ]] && return 0
|
||||||
|
grep -rqs '^wifi.backend=' /etc/NetworkManager/NetworkManager.conf /etc/NetworkManager/conf.d && return 0
|
||||||
|
printf '%s\n' "# Written by Steamify: Steam's Wi-Fi backend setting (steamos-manager)." \
|
||||||
|
'[device]' 'wifi.backend=wpa_supplicant' | sudo install -Dm644 /dev/stdin "$WIFI_BACKEND_CONF" ||
|
||||||
|
{ err "Writing the Wi-Fi backend setting failed."; return 1; }
|
||||||
|
state_set machine wifi_backend 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_backend_disable() {
|
||||||
|
[[ -n "$(state_get machine wifi_backend)" ]] || return 0
|
||||||
|
sudo rm -f "$WIFI_BACKEND_CONF"
|
||||||
|
state_clear machine
|
||||||
|
}
|
||||||
|
|
||||||
|
serial_enable() {
|
||||||
|
printf '%s\n' "# Written by Steamify: Steam shows the serial number, like on SteamOS." \
|
||||||
|
'z /sys/class/dmi/id/product_serial 0444 - - -' | sudo tee "$SERIAL_TMPFILES" > /dev/null &&
|
||||||
|
sudo systemd-tmpfiles --create "$SERIAL_TMPFILES" ||
|
||||||
|
{ err "Making the serial number readable for Steam failed."; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
serial_disable() {
|
||||||
|
sudo rm -f "$SERIAL_TMPFILES"
|
||||||
|
sudo chmod 0400 /sys/class/dmi/id/product_serial 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
machine_enable() {
|
machine_enable() {
|
||||||
# The pinned kernel first when it's wanted too, so DKMS builds the LED
|
# The pinned kernel first when it's wanted too, so DKMS builds the LED
|
||||||
# driver for it once, instead of for the current kernel and then again.
|
# driver for it once, instead of for the current kernel and then again.
|
||||||
@@ -401,9 +443,11 @@ EOF
|
|||||||
# for hardware and controller settings; it knows the Steam Machine from its DMI data.
|
# for hardware and controller settings; it knows the Steam Machine from its DMI data.
|
||||||
info "Installing hardware manager packages (steamos-manager & inputplumber)..."
|
info "Installing hardware manager packages (steamos-manager & inputplumber)..."
|
||||||
sudo pacman -S --needed --noconfirm steamos-manager inputplumber || { err "Installing hardware manager packages failed."; return 1; }
|
sudo pacman -S --needed --noconfirm steamos-manager inputplumber || { err "Installing hardware manager packages failed."; return 1; }
|
||||||
|
serial_enable || return 1
|
||||||
|
wifi_backend_enable || return 1
|
||||||
sudo systemctl enable --now inputplumber.service
|
sudo systemctl enable --now inputplumber.service
|
||||||
sudo systemctl enable --now steamos-manager.service
|
sudo systemctl enable --now steamos-manager.service
|
||||||
systemctl --user enable steamos-manager.service 2>/dev/null
|
user_systemctl enable steamos-manager.service 2>/dev/null
|
||||||
# HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet.
|
# HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet.
|
||||||
cec_status && cec_link_steamos_manager
|
cec_status && cec_link_steamos_manager
|
||||||
|
|
||||||
@@ -420,10 +464,12 @@ EOF
|
|||||||
|
|
||||||
machine_disable() {
|
machine_disable() {
|
||||||
info "Removing Steam Machine support..."
|
info "Removing Steam Machine support..."
|
||||||
systemctl --user disable --now steamos-manager.service 2>/dev/null
|
user_systemctl disable --now steamos-manager.service 2>/dev/null
|
||||||
sudo systemctl disable --now steamos-manager.service 2>/dev/null
|
sudo systemctl disable --now steamos-manager.service 2>/dev/null
|
||||||
sudo systemctl disable --now inputplumber.service 2>/dev/null
|
sudo systemctl disable --now inputplumber.service 2>/dev/null
|
||||||
sudo pacman -Rns --noconfirm steamos-manager inputplumber 2>/dev/null
|
sudo pacman -Rns --noconfirm steamos-manager inputplumber 2>/dev/null
|
||||||
|
serial_disable
|
||||||
|
wifi_backend_disable
|
||||||
sudo rm -f "$LED_UDEV_RULE" /etc/modules-load.d/leds-valve.conf
|
sudo rm -f "$LED_UDEV_RULE" /etc/modules-load.d/leds-valve.conf
|
||||||
sudo udevadm control --reload
|
sudo udevadm control --reload
|
||||||
sudo modprobe -r leds-valve 2>/dev/null
|
sudo modprobe -r leds-valve 2>/dev/null
|
||||||
@@ -431,8 +477,9 @@ machine_disable() {
|
|||||||
krevert machine
|
krevert machine
|
||||||
reload_powerdevil
|
reload_powerdevil
|
||||||
sudo systemctl disable ensure-kernel-headers.service 2>/dev/null
|
sudo systemctl disable ensure-kernel-headers.service 2>/dev/null
|
||||||
sudo rm -f "$LED_DKMS_OVERRIDE" "$HEADERS_UNIT" "$HEADERS_SCRIPT"
|
led_dkms_override_remove
|
||||||
sudo rmdir "$(dirname "$HEADERS_SCRIPT")" 2>/dev/null
|
sudo rm -f "$HEADERS_UNIT" "$HEADERS_SCRIPT"
|
||||||
|
sudo rm -rf "$(dirname "$HEADERS_SCRIPT")" "$(dirname "$HEADERS_SCRIPT_OLD")"
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
ok "Steam Machine support removed (the AUR helper, if installed, is kept)."
|
ok "Steam Machine support removed (the AUR helper, if installed, is kept)."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/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_SCRIPT="$STEAMIFY_BIN/steamify-notifier"
|
||||||
|
NOTIFY_UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
|
NOTIFY_NAME=steamify-update-check
|
||||||
|
|
||||||
|
notify_status() {
|
||||||
|
[[ -f "$NOTIFY_SCRIPT" ]] &&
|
||||||
|
user_systemctl is-enabled -q "$NOTIFY_NAME.timer" 2>/dev/null &&
|
||||||
|
user_systemctl 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. Also whether it
|
||||||
|
# was the app or the terminal menu: the notification opens the same.
|
||||||
|
notify_status || return 0
|
||||||
|
state_set notify seen "$VERSION"
|
||||||
|
if [[ "$BACKEND" == true ]]; then state_set notify frontend app; else state_set notify frontend terminal; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
service_file "$NOTIFY_NAME.service" NOTIFY_SCRIPT="$NOTIFY_SCRIPT" > "$NOTIFY_UNITS/$NOTIFY_NAME.service"
|
||||||
|
service_file "$NOTIFY_NAME.timer" > "$NOTIFY_UNITS/$NOTIFY_NAME.timer"
|
||||||
|
user_systemctl daemon-reload
|
||||||
|
user_systemctl enable "$NOTIFY_NAME.service" >/dev/null 2>&1 &&
|
||||||
|
user_systemctl 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() {
|
||||||
|
user_systemctl 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 "$STEAMIFY_BIN" 2>/dev/null || true
|
||||||
|
user_systemctl daemon-reload
|
||||||
|
state_clear notify
|
||||||
|
ok "Update notifications off."
|
||||||
|
}
|
||||||
+19
-1
@@ -54,10 +54,18 @@ theme_enable() {
|
|||||||
# Only remove it again on disable if we were the ones installing it.
|
# Only remove it again on disable if we were the ones installing it.
|
||||||
state_set theme installed_pkg 1
|
state_set theme installed_pkg 1
|
||||||
fi
|
fi
|
||||||
|
# Without a session (the ISO's installer), for a user who has never
|
||||||
|
# logged in: the layout still CachyOS's default from /etc/skel is
|
||||||
|
# removed (after the backup below), and Plasma builds its first layout
|
||||||
|
# from the look-and-feel package, as --resetLayout does below.
|
||||||
|
local offline=false layout=~/.config/plasma-org.kde.plasma.desktop-appletsrc
|
||||||
if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
|
if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
|
||||||
|
if [[ -e "$layout" ]] && ! cmp -s "$layout" "/etc/skel/.config/${layout##*/}"; then
|
||||||
err "Run this from the Plasma desktop: applying the Vapor layout needs a running Plasma session."
|
err "Run this from the Plasma desktop: applying the Vapor layout needs a running Plasma session."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
offline=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Only the first time, so a re-apply doesn't overwrite the originals.
|
# Only the first time, so a re-apply doesn't overwrite the originals.
|
||||||
local backup="$STATE_DIR/theme-layout" f
|
local backup="$STATE_DIR/theme-layout" f
|
||||||
@@ -87,6 +95,12 @@ theme_enable() {
|
|||||||
kset theme kdeglobals General ColorScheme --delete
|
kset theme kdeglobals General ColorScheme --delete
|
||||||
plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true
|
plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if [[ "$offline" == true ]]; then
|
||||||
|
rm -f ~/.config/plasma-org.kde.plasma.desktop-appletsrc ~/.config/plasmashellrc
|
||||||
|
extras_enable || warn "SteamOS desktop extras not installed (see above); the theme itself is on."
|
||||||
|
ok "Vapor theme set; Plasma lays out the Vapor desktop at the first login."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
info "Applying the Vapor theme with its desktop and window layout..."
|
info "Applying the Vapor theme with its desktop and window layout..."
|
||||||
# --resetLayout is "Desktop and window layout" in System Settings: the
|
# --resetLayout is "Desktop and window layout" in System Settings: the
|
||||||
# SteamOS panel, launcher icon and wallpaper. It needs plasmashell up.
|
# SteamOS panel, launcher icon and wallpaper. It needs plasmashell up.
|
||||||
@@ -120,7 +134,11 @@ theme_disable() {
|
|||||||
fi
|
fi
|
||||||
# After the tools above, so keys that weren't set before are removed again.
|
# After the tools above, so keys that weren't set before are removed again.
|
||||||
krevert theme
|
krevert theme
|
||||||
single_status && single_launcher
|
if single_status; then
|
||||||
|
single_launcher
|
||||||
|
else
|
||||||
|
single_launcher_drop
|
||||||
|
fi
|
||||||
restart_plasmashell_if_stopped
|
restart_plasmashell_if_stopped
|
||||||
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
|
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
+74
-9
@@ -4,7 +4,7 @@
|
|||||||
# are evicted first, so a game that needs most of it doesn't spill into
|
# are evicted first, so a game that needs most of it doesn't spill into
|
||||||
# system RAM and stutter. The kernel side is the dmem cgroup controller
|
# system RAM and stutter. The kernel side is the dmem cgroup controller
|
||||||
# (7.2; amdgpu and xe register their VRAM, NVIDIA's driver doesn't), so it's
|
# (7.2; amdgpu and xe register their VRAM, NVIDIA's driver doesn't), so it's
|
||||||
# only offered when the kernel lists a VRAM region of at least 2 GB. CachyOS packages the userspace
|
# only offered when the kernel lists a VRAM region (vram or vidmem) of at least 2 GB. CachyOS packages the userspace
|
||||||
# side: dmemcg-booster (a system service that enables the controller and
|
# side: dmemcg-booster (a system service that enables the controller and
|
||||||
# sets the limits, plus a user service) and plasma-foreground-booster, which
|
# sets the limits, plus a user service) and plasma-foreground-booster, which
|
||||||
# tells it which window is in front on the desktop. The latter only starts
|
# tells it which window is in front on the desktop. The latter only starts
|
||||||
@@ -13,14 +13,21 @@
|
|||||||
|
|
||||||
VRAM_PKGS=(dmemcg-booster plasma-foreground-booster)
|
VRAM_PKGS=(dmemcg-booster plasma-foreground-booster)
|
||||||
VRAM_MIN_BYTES=$((2 * 1024 * 1024 * 1024))
|
VRAM_MIN_BYTES=$((2 * 1024 * 1024 * 1024))
|
||||||
|
# The regions the kernel's dmem controller lists (a file under /sys/fs/cgroup;
|
||||||
|
# a variable so tests can point it at a copy).
|
||||||
|
VRAM_CAPACITY="${WIZARD_VRAM_CAPACITY:-/sys/fs/cgroup/dmem.capacity}"
|
||||||
|
|
||||||
vram_supported() {
|
vram_supported() {
|
||||||
# WIZARD_VRAM_FAKE_NVIDIA=1 (tests): as with an NVIDIA card.
|
# WIZARD_VRAM_FAKE_NVIDIA (tests): as with an NVIDIA card whose driver
|
||||||
|
# doesn't register its VRAM; see vram_nvidia_case.
|
||||||
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 1
|
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 1
|
||||||
# Only for a GPU with its own VRAM: an integrated GPU registers a small
|
# Only for a GPU with its own VRAM: an integrated GPU registers a small
|
||||||
# carve-out too, but mostly uses system RAM, so there's little to boost.
|
# carve-out too, but mostly uses system RAM, so there's little to boost.
|
||||||
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/vram$/ && $2 >= min { found = 1 } END { exit !found }' \
|
# By what the driver registers, not the brand: amdgpu and xe name the
|
||||||
/sys/fs/cgroup/dmem.capacity 2>/dev/null
|
# region "vram", other drivers "vidmem" (numbered with several), so an
|
||||||
|
# NVIDIA driver that registers its memory is offered right away.
|
||||||
|
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/(vram|vidmem)[0-9]*$/ && $2 >= min { found = 1 } END { exit !found }' \
|
||||||
|
"$VRAM_CAPACITY" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
vram_nvidia() {
|
vram_nvidia() {
|
||||||
@@ -33,7 +40,65 @@ vram_nvidia() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Shown where it works, and greyed out with an NVIDIA card, whose driver
|
# chwd's lists of NVIDIA cards that need a closed legacy branch (580xx,
|
||||||
|
# 470xx, 390xx); every other NVIDIA card runs the open kernel modules.
|
||||||
|
VRAM_CHWD_IDS=/var/lib/chwd/ids
|
||||||
|
|
||||||
|
vram_nvidia_legacy_id() {
|
||||||
|
# vram_nvidia_legacy_id <pci device id, with or without 0x>: 0 when chwd
|
||||||
|
# lists the card for a closed legacy branch, i.e. it is older than the
|
||||||
|
# RTX 20 series. Also what lib/nvidia.sh uses to decide if a card is supported.
|
||||||
|
grep -qwi "${1#0x}" "$VRAM_CHWD_IDS"/nvidia-*.ids 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
vram_nvidia_case() {
|
||||||
|
# Why an NVIDIA card's VRAM isn't registered, so the menu can say what to
|
||||||
|
# do: "update" (open modules older than 615), "switch <chwd profile>"
|
||||||
|
# (closed driver on a card the open one supports), "legacy" (card only
|
||||||
|
# runs a closed legacy branch), "nouveau" (no NVIDIA module).
|
||||||
|
# WIZARD_VRAM_FAKE_NVIDIA=1 fakes "switch nvidia-dkms-580xx", or name a case.
|
||||||
|
local fake="${WIZARD_VRAM_FAKE_NVIDIA:-}" license d id p
|
||||||
|
if [[ -n "$fake" ]]; then
|
||||||
|
[[ "$fake" == 1 ]] && fake="switch nvidia-dkms-580xx"
|
||||||
|
echo "$fake"; return
|
||||||
|
fi
|
||||||
|
license="$(modinfo -F license nvidia 2>/dev/null)"
|
||||||
|
case "$license" in
|
||||||
|
"Dual MIT/GPL") echo update; return ;;
|
||||||
|
"") echo nouveau; return ;;
|
||||||
|
esac
|
||||||
|
for d in /sys/bus/pci/devices/*; do
|
||||||
|
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
|
||||||
|
vram_nvidia_legacy_id "$(cat "$d/device")" && { echo legacy; return; }
|
||||||
|
done
|
||||||
|
for p in 580xx 470xx 390xx; do
|
||||||
|
pacman -Q "nvidia-$p-dkms" >/dev/null 2>&1 && { echo "switch nvidia-dkms-$p"; return; }
|
||||||
|
done
|
||||||
|
echo switch
|
||||||
|
}
|
||||||
|
|
||||||
|
vram_nvidia_hint() {
|
||||||
|
# vram_nvidia_hint <case>: the short reason (terminal menu, the app's row).
|
||||||
|
case "$1" in
|
||||||
|
update) echo "update NVIDIA's driver to 615 or newer (sudo pacman -Syu)" ;;
|
||||||
|
switch*) echo "needs NVIDIA's open driver, which your card supports" ;;
|
||||||
|
legacy) echo "your NVIDIA card's driver doesn't support it (older than RTX 20)" ;;
|
||||||
|
*) echo "the nouveau driver doesn't support it yet" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
vram_nvidia_note() {
|
||||||
|
# vram_nvidia_note <case>: the full explanation, with what to do.
|
||||||
|
local profile="${1#switch}"; profile="${profile# }"
|
||||||
|
case "$1" in
|
||||||
|
update) echo "NVIDIA's open driver tells Linux how its video memory is used from version 615 on; yours is older ($(cat /sys/module/nvidia/version 2>/dev/null || echo unknown)). Update your system (sudo pacman -Syu), restart, and it's available here." ;;
|
||||||
|
switch*) echo "Your card also runs NVIDIA's open driver, the one CachyOS installs by default, and only that one tells Linux how its video memory is used. You're using NVIDIA's closed driver. Switch in a terminal with: ${profile:+sudo chwd -r $profile && }sudo chwd -i nvidia-open-dkms, then restart: it's available here after that. Steamify doesn't switch it for you: if something goes wrong, the screen stays black after the restart." ;;
|
||||||
|
legacy) echo "Your NVIDIA card is older than the RTX 20 series and only runs NVIDIA's closed driver, which doesn't tell Linux how its video memory is used. Newer NVIDIA cards (RTX 20 series and up), and AMD and Intel graphics cards, support it." ;;
|
||||||
|
*) echo "The open-source nouveau driver doesn't tell Linux how its video memory is used yet (a patch is on its way into the kernel). NVIDIA's own open driver does, from version 615 on (sudo chwd -a installs the right one)." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shown where it works, and greyed out with an NVIDIA card whose driver
|
||||||
# doesn't register its VRAM with the kernel (yet): so it's clear why it
|
# doesn't register its VRAM with the kernel (yet): so it's clear why it
|
||||||
# can't be turned on. Hidden elsewhere (integrated graphics, older kernel).
|
# can't be turned on. Hidden elsewhere (integrated graphics, older kernel).
|
||||||
vram_available() { vram_supported || vram_status || vram_nvidia; }
|
vram_available() { vram_supported || vram_status || vram_nvidia; }
|
||||||
@@ -42,7 +107,7 @@ vram_selectable() { vram_supported || vram_status; }
|
|||||||
vram_status() {
|
vram_status() {
|
||||||
pacman -Q "${VRAM_PKGS[@]}" >/dev/null 2>&1 &&
|
pacman -Q "${VRAM_PKGS[@]}" >/dev/null 2>&1 &&
|
||||||
systemctl is-enabled -q dmemcg-booster-system.service 2>/dev/null &&
|
systemctl is-enabled -q dmemcg-booster-system.service 2>/dev/null &&
|
||||||
systemctl --user is-enabled -q dmemcg-booster-user.service 2>/dev/null
|
user_systemctl is-enabled -q dmemcg-booster-user.service 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
vram_enable() {
|
vram_enable() {
|
||||||
@@ -56,18 +121,18 @@ vram_enable() {
|
|||||||
fi
|
fi
|
||||||
sudo systemctl enable --now dmemcg-booster-system.service >/dev/null 2>&1 ||
|
sudo systemctl enable --now dmemcg-booster-system.service >/dev/null 2>&1 ||
|
||||||
{ err "Starting dmemcg-booster failed."; return 1; }
|
{ err "Starting dmemcg-booster failed."; return 1; }
|
||||||
systemctl --user enable --now dmemcg-booster-user.service >/dev/null 2>&1 ||
|
user_systemctl enable --now dmemcg-booster-user.service >/dev/null 2>&1 ||
|
||||||
{ err "Starting dmemcg-booster for $USER failed."; return 1; }
|
{ err "Starting dmemcg-booster for $USER failed."; return 1; }
|
||||||
kset vram kcgroupsrc "Foreground Booster" autostart true
|
kset vram kcgroupsrc "Foreground Booster" autostart true
|
||||||
# Started by the Plasma session from now on; start it in this one too.
|
# Started by the Plasma session from now on; start it in this one too.
|
||||||
[[ "${XDG_CURRENT_DESKTOP:-}" == KDE ]] &&
|
[[ "${XDG_CURRENT_DESKTOP:-}" == KDE ]] &&
|
||||||
systemctl --user start plasma-foreground-booster.service >/dev/null 2>&1
|
user_systemctl start plasma-foreground-booster.service >/dev/null 2>&1
|
||||||
ok "VRAM booster on: the game in front keeps its VRAM."
|
ok "VRAM booster on: the game in front keeps its VRAM."
|
||||||
}
|
}
|
||||||
|
|
||||||
vram_disable() {
|
vram_disable() {
|
||||||
local pkgs
|
local pkgs
|
||||||
systemctl --user disable --now plasma-foreground-booster.service dmemcg-booster-user.service >/dev/null 2>&1
|
user_systemctl disable --now plasma-foreground-booster.service dmemcg-booster-user.service >/dev/null 2>&1
|
||||||
sudo systemctl disable --now dmemcg-booster-system.service >/dev/null 2>&1
|
sudo systemctl disable --now dmemcg-booster-system.service >/dev/null 2>&1
|
||||||
krevert vram
|
krevert vram
|
||||||
pkgs="$(state_get vram installed_pkgs)"
|
pkgs="$(state_get vram installed_pkgs)"
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ WIZARD_RELEASE="https://github.com/theupriser/steamify-cachyos/releases/latest/d
|
|||||||
WIZARD_URL="$WIZARD_RELEASE/steamify.sh"
|
WIZARD_URL="$WIZARD_RELEASE/steamify.sh"
|
||||||
WIZARD_APP_URL="$WIZARD_RELEASE/steamify-app.sh"
|
WIZARD_APP_URL="$WIZARD_RELEASE/steamify-app.sh"
|
||||||
# Steam logo with a gear (assets/ in the repo, published with every release).
|
# Steam logo with a gear (assets/ in the repo, published with every release).
|
||||||
WIZARD_ICON="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/cachyos-gamescope-boot-wizard.svg"
|
WIZARD_ICON="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/steamify.svg"
|
||||||
WIZARD_LAUNCHER="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot/run-wizard"
|
WIZARD_LAUNCHER="$STEAMIFY_BIN/run-wizard"
|
||||||
WIZARD_APP_LAUNCHER="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot/run-app"
|
WIZARD_APP_LAUNCHER="$STEAMIFY_BIN/run-app"
|
||||||
WIZARD_APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
WIZARD_APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||||
# steamify-ui: the id the app gives itself, so its window belongs to this
|
# steamify-ui: the id the app gives itself, so its window belongs to this
|
||||||
# entry. steamify-app.sh leaves an entry with X-Steamify-Shortcut alone.
|
# entry. steamify-app.sh leaves an entry with X-Steamify-Shortcut alone.
|
||||||
@@ -91,7 +91,7 @@ EOF
|
|||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
if curl -fsSL --max-time 20 "$WIZARD_RELEASE/steam-gaming-settings.svg" -o "$tmp" && grep -q '<svg' "$tmp"; then
|
if curl -fsSL --max-time 20 "$WIZARD_RELEASE/steam-gaming-settings.svg" -o "$tmp" && grep -q '<svg' "$tmp"; then
|
||||||
install -D -m 644 "$tmp" "$WIZARD_ICON"
|
install -D -m 644 "$tmp" "$WIZARD_ICON"
|
||||||
icon=cachyos-gamescope-boot-wizard
|
icon=steamify
|
||||||
gtk-update-icon-cache -q -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true
|
gtk-update-icon-cache -q -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
warn "Couldn't download the shortcut's icon; using Steam's icon instead."
|
warn "Couldn't download the shortcut's icon; using Steam's icon instead."
|
||||||
|
|||||||
+9
-1
@@ -2,9 +2,17 @@
|
|||||||
|
|
||||||
Files Steamify builds or applies, kept out of the shell code so they can be
|
Files Steamify builds or applies, kept out of the shell code so they can be
|
||||||
read and reviewed on their own. `lib/*.sh` reads them with `patch_file
|
read and reviewed on their own. `lib/*.sh` reads them with `patch_file
|
||||||
<name>`; the single-file build (`.github/tools/bundle.sh`) embeds them.
|
<name>` (piped through `fill KEY=value...` for `@KEY@` placeholders); the single-file build (`.github/tools/bundle.sh`) embeds them.
|
||||||
|
|
||||||
| File | Used by | What |
|
| 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-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 |
|
||||||
|
| `sync-steamos-session.sh` | `lib/login-manager.sh` (SteamOS conversion, plasma-login-manager) | The sync bridge: copies Steam's session choice from `zz-steamos-autologin.conf` into `plasmalogin.conf`'s `[Autologin]` |
|
||||||
|
| `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 |
|
||||||
|
| `steamify-os-release.sh`, `steamify-os-release.hook` | `lib/login-manager.sh` (SteamOS conversion) | What Steam's System settings show besides the OS name (which stays CachyOS's): `VERSION_ID=steamos-X.Y`, `VERSION_CODENAME=steam-machine`, `VARIANT`/`VARIANT_ID` Steamify with its version, in `/etc/os-release`; the pacman hook runs it again after cachyos-hooks (`fill SCRIPT=... VERSION=... STEAMOS=...`) |
|
||||||
|
| `steamify-nvidia-initramfs.sh`, `steamify-nvidia-initramfs.hook` | `lib/nvidia.sh` (SteamOS conversion, NVIDIA fix) | The pacman hook (before the initramfs is built, after DKMS) and its script: the NVIDIA modules go into the initramfs only while every installed kernel has them, decided again at every kernel or driver change (`fill SCRIPT=...`) |
|
||||||
| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port |
|
| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port |
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Installed by Steamify: install missing -headers for every
|
||||||
|
# installed kernel, so DKMS builds leds-valve for it.
|
||||||
|
set -u
|
||||||
|
missing=()
|
||||||
|
for k in $(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u); do
|
||||||
|
pacman -Q "${k}-headers" >/dev/null 2>&1 && continue
|
||||||
|
pacman -Si "${k}-headers" >/dev/null 2>&1 && missing+=("${k}-headers")
|
||||||
|
done
|
||||||
|
[[ ${#missing[@]} -eq 0 ]] && exit 0
|
||||||
|
# Another pacman is running (e.g. an update): try again next boot.
|
||||||
|
[[ -e /var/lib/pacman/db.lck ]] && { echo "pacman is busy, skipping"; exit 0; }
|
||||||
|
echo "Installing missing kernel headers: ${missing[*]}"
|
||||||
|
exec pacman -S --needed --noconfirm "${missing[@]}"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Written by Steamify: build leds-valve for DKMS's target kernel, not the running one.
|
||||||
|
MAKE[0]="make KVERSION=${kernelver}"
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Steamify CachyOS: Steam's non-Steam games list (userdata/<id>/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 <vdf> <exe> -> appid of an entry for exe
|
||||||
|
# steam-shortcuts.py add <vdf> <name> <exe> <icon> <desktop file>
|
||||||
|
# -> "added <appid>" / "updated <appid>"
|
||||||
|
# steam-shortcuts.py remove <vdf> <appid>
|
||||||
|
# An entry counts as ours when its Exe is <exe>, 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("<I", data, pos)[0]))
|
||||||
|
pos += 4
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown VDF type {t} at {pos}")
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return [("shortcuts", [])]
|
||||||
|
root = block()
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def dump(items):
|
||||||
|
out = bytearray()
|
||||||
|
for key, value in items:
|
||||||
|
k = key.encode("utf-8", "surrogateescape") + b"\0"
|
||||||
|
if isinstance(value, list):
|
||||||
|
out += bytes([MAP]) + k + dump(value)
|
||||||
|
elif isinstance(value, int):
|
||||||
|
out += bytes([INT]) + k + struct.pack("<I", value & 0xFFFFFFFF)
|
||||||
|
else:
|
||||||
|
out += bytes([STR]) + k + value.encode("utf-8", "surrogateescape") + b"\0"
|
||||||
|
return bytes(out) + bytes([END])
|
||||||
|
|
||||||
|
|
||||||
|
def get(entry, key, default=None):
|
||||||
|
# Steam's key names vary in case between versions (appid/AppName/Exe).
|
||||||
|
for k, v in entry:
|
||||||
|
if k.lower() == key.lower():
|
||||||
|
return v
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def put(entry, key, value):
|
||||||
|
for i, (k, _) in enumerate(entry):
|
||||||
|
if k.lower() == key.lower():
|
||||||
|
entry[i] = (k, value)
|
||||||
|
return
|
||||||
|
entry.append((key, value))
|
||||||
|
|
||||||
|
|
||||||
|
def shortcuts(root):
|
||||||
|
for k, v in root:
|
||||||
|
if k.lower() == "shortcuts":
|
||||||
|
return v
|
||||||
|
raise ValueError("no shortcuts block")
|
||||||
|
|
||||||
|
|
||||||
|
def ours(entry, exe):
|
||||||
|
have = get(entry, "Exe", "").strip('"')
|
||||||
|
return have == exe or (have.endswith("/run-app") and
|
||||||
|
("/steamify/" in have or "/cachyos-gamescope-boot/" in have))
|
||||||
|
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return parse(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
return [("shortcuts", [])]
|
||||||
|
|
||||||
|
|
||||||
|
def save(path, root):
|
||||||
|
tmp = path + ".steamify-tmp"
|
||||||
|
with open(tmp, "wb") as f:
|
||||||
|
f.write(dump(root))
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
cmd, path = argv[1], argv[2]
|
||||||
|
root = load(path)
|
||||||
|
entries = shortcuts(root)
|
||||||
|
if cmd == "find":
|
||||||
|
for _, e in entries:
|
||||||
|
if ours(e, argv[3]):
|
||||||
|
print(get(e, "appid", 0))
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
if cmd == "add":
|
||||||
|
name, exe, icon, desktop = argv[3:7]
|
||||||
|
for _, e in entries:
|
||||||
|
if ours(e, exe):
|
||||||
|
put(e, "AppName", name)
|
||||||
|
put(e, "Exe", f'"{exe}"')
|
||||||
|
put(e, "StartDir", f'"{os.path.dirname(exe)}/"')
|
||||||
|
put(e, "icon", icon)
|
||||||
|
put(e, "ShortcutPath", desktop)
|
||||||
|
save(path, root)
|
||||||
|
print("updated", get(e, "appid", 0))
|
||||||
|
return 0
|
||||||
|
# Steam's own id for a shortcut: CRC32 of exe and name, top bit set.
|
||||||
|
appid = (zlib.crc32(f'"{exe}"{name}'.encode()) | 0x80000000) & 0xFFFFFFFF
|
||||||
|
entry = [("appid", appid), ("AppName", name), ("Exe", f'"{exe}"'),
|
||||||
|
("StartDir", f'"{os.path.dirname(exe)}/"'), ("icon", icon),
|
||||||
|
("ShortcutPath", desktop), ("LaunchOptions", ""), ("IsHidden", 0),
|
||||||
|
("AllowDesktopConfig", 1), ("AllowOverlay", 1), ("OpenVR", 0),
|
||||||
|
("Devkit", 0), ("DevkitGameID", ""), ("DevkitOverrideAppID", 0),
|
||||||
|
("LastPlayTime", 0), ("FlatpakAppID", ""), ("tags", [])]
|
||||||
|
entries.append((str(len(entries)), entry))
|
||||||
|
if os.path.dirname(path):
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
save(path, root)
|
||||||
|
print("added", appid)
|
||||||
|
return 0
|
||||||
|
if cmd == "remove":
|
||||||
|
keep = [e for _, e in entries if str(get(e, "appid", "")) != argv[3]]
|
||||||
|
if len(keep) == len(entries):
|
||||||
|
return 0
|
||||||
|
entries[:] = [(str(i), e) for i, e in enumerate(keep)]
|
||||||
|
save(path, root)
|
||||||
|
return 0
|
||||||
|
print(f"unknown command {cmd}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Written by Steamify: Valve's cros_ec_cec, which knows the Steam Machine.
|
||||||
|
PACKAGE_NAME="@NAME@"
|
||||||
|
PACKAGE_VERSION="@VERSION@"
|
||||||
|
BUILT_MODULE_NAME[0]="cros-ec-cec"
|
||||||
|
DEST_MODULE_LOCATION[0]="/updates/dkms"
|
||||||
|
AUTOINSTALL="yes"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Written by Steamify: the Steam Machine powers off instead of booting up again.
|
||||||
|
PACKAGE_NAME="@NAME@"
|
||||||
|
PACKAGE_VERSION="@VERSION@"
|
||||||
|
BUILT_MODULE_NAME[0]="@NAME@"
|
||||||
|
DEST_MODULE_LOCATION[0]="/updates/dkms"
|
||||||
|
AUTOINSTALL="yes"
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#!/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"),
|
||||||
|
"steamify")
|
||||||
|
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, "steamify", "bin", "run-app")
|
||||||
|
RUN_TERMINAL = os.path.join(DATA, "steamify", "bin", "run-wizard")
|
||||||
|
RELEASE = f"https://github.com/{REPO}/releases/latest/download"
|
||||||
|
|
||||||
|
|
||||||
|
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("steamify", QIcon.fromTheme("steam"))
|
||||||
|
|
||||||
|
def open_app():
|
||||||
|
# The way Steamify was last used (the app or the terminal menu), the
|
||||||
|
# newest release either way; it records its version as seen, which
|
||||||
|
# ends the reminder. In its own unit: this one's processes are
|
||||||
|
# stopped when the check exits.
|
||||||
|
if state("frontend") == "terminal":
|
||||||
|
cmd = ["konsole", "-e", RUN_TERMINAL] if os.access(RUN_TERMINAL, os.X_OK) else \
|
||||||
|
["konsole", "-e", "bash", "-c",
|
||||||
|
f"set -o pipefail; curl -fsSL --max-time 30 {RELEASE}/steamify.sh | bash; read -rp 'Press Enter to close. ' _"]
|
||||||
|
else:
|
||||||
|
cmd = [RUN_APP] if os.access(RUN_APP, os.X_OK) else \
|
||||||
|
["bash", "-c", f"set -o pipefail; curl -fsSL --max-time 30 {RELEASE}/steamify-app.sh | bash"]
|
||||||
|
# A scope lives as long as anything in it: the start script puts the
|
||||||
|
# app in the background and exits.
|
||||||
|
subprocess.Popen(["systemd-run", "--user", "--scope", "--collect", "--quiet", *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=steamify",
|
||||||
|
"-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())
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix): decides
|
||||||
|
# again whether the NVIDIA modules go into the initramfs, after DKMS has built
|
||||||
|
# its modules (71-dkms-install) and before the initramfs is built
|
||||||
|
# (90-mkinitcpio-install); hooks run in file name order.
|
||||||
|
[Trigger]
|
||||||
|
Operation = Install
|
||||||
|
Operation = Upgrade
|
||||||
|
Operation = Remove
|
||||||
|
Type = Path
|
||||||
|
Target = usr/lib/modules/*/vmlinuz
|
||||||
|
Target = usr/lib/modules/*/extramodules/nvidia*
|
||||||
|
Target = usr/lib/modules/*/updates/*
|
||||||
|
|
||||||
|
[Action]
|
||||||
|
Description = Steamify: NVIDIA modules in the initramfs only if every kernel has them
|
||||||
|
When = PostTransaction
|
||||||
|
Exec = @SCRIPT@ sync
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix), run by the
|
||||||
|
# pacman hook steamify-nvidia-initramfs.hook before the initramfs is built.
|
||||||
|
# The NVIDIA modules go into the initramfs (early KMS) only while EVERY
|
||||||
|
# installed kernel has them: mkinitcpio fails on a module a kernel lacks, and
|
||||||
|
# limine-mkinitcpio then skips that kernel's boot entry, kernel parameters
|
||||||
|
# included. Decided again at every kernel or driver change, so a new kernel
|
||||||
|
# without the modules (another kernel, a failed DKMS build, a downgrade) gets
|
||||||
|
# a working initramfs and the file comes back once it has them.
|
||||||
|
# steamify-nvidia-initramfs [sync|check]
|
||||||
|
# sync (default) write or remove the drop-in to match
|
||||||
|
# check exit status 0 when every installed kernel has nvidia_drm
|
||||||
|
conf="${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf}"
|
||||||
|
mods="${NVIDIA_MODULES_DIR:-/usr/lib/modules}"
|
||||||
|
|
||||||
|
everywhere() {
|
||||||
|
local k found=0
|
||||||
|
for k in "$mods"/*/; do
|
||||||
|
[[ -f "$k/vmlinuz" || -f "$k/pkgbase" ]] || continue # an installed kernel, not a leftover folder
|
||||||
|
find "$k" -name 'nvidia-drm.ko*' -print -quit 2>/dev/null | grep -q . || return 1
|
||||||
|
found=1
|
||||||
|
done
|
||||||
|
[[ $found == 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-sync}" in
|
||||||
|
check) everywhere ;;
|
||||||
|
sync)
|
||||||
|
if everywhere; then
|
||||||
|
[[ -f "$conf" ]] || { mkdir -p "$(dirname "$conf")" &&
|
||||||
|
echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' > "$conf"; }
|
||||||
|
else
|
||||||
|
rm -f "$conf"
|
||||||
|
fi ;;
|
||||||
|
*) echo "usage: $0 [sync|check]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Installed by Steamify CachyOS (SteamOS conversion): runs after
|
||||||
|
# cachyos-hooks' os-release/cachyos-branding hooks (hooks run in file name
|
||||||
|
# order).
|
||||||
|
[Trigger]
|
||||||
|
Operation = Install
|
||||||
|
Operation = Upgrade
|
||||||
|
Type = Package
|
||||||
|
Target = filesystem
|
||||||
|
Target = cachyos-hooks
|
||||||
|
|
||||||
|
[Action]
|
||||||
|
Description = Steamify: the OS variant and version Steam shows
|
||||||
|
When = PostTransaction
|
||||||
|
Exec = @SCRIPT@
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Installed by Steamify CachyOS (SteamOS conversion): what Steam shows in its
|
||||||
|
# System settings besides the OS name, which stays CachyOS's: the SteamOS
|
||||||
|
# release Steamify follows as the OS version (VERSION_ID, e.g. steamos-3.9),
|
||||||
|
# steam-machine as the codename, and Steamify with its version as the variant
|
||||||
|
# (VARIANT_ID, which Steam shows). KDE's About this System would show
|
||||||
|
# VERSION_ID after the name: the conversion sets its UseOSReleaseVersion, so it
|
||||||
|
# shows os-release's VERSION instead, which CachyOS doesn't have. NAME and
|
||||||
|
# PRETTY_NAME are never touched: limine-snapper-sync finds the boot entries by
|
||||||
|
# the OS name. The zz-steamify-os-release hook runs this after cachyos-hooks.
|
||||||
|
sed -i -e '/^VARIANT=/d' -e '/^VARIANT_ID=/d' -e '/^VERSION_ID=/d' -e '/^VERSION_CODENAME=/d' /etc/os-release
|
||||||
|
printf '%s\n' 'VARIANT="Steamify @VERSION@"' 'VARIANT_ID=steamify-@VERSION@' 'VERSION_CODENAME=steam-machine' >> /etc/os-release
|
||||||
|
[ -n "@STEAMOS@" ] && echo 'VERSION_ID=@STEAMOS@' >> /etc/os-release
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
SRC="/etc/plasmalogin.conf.d/zz-steamos-autologin.conf"
|
||||||
|
DEST="/etc/plasmalogin.conf"
|
||||||
|
|
||||||
|
[[ -f "$SRC" ]] || exit 0
|
||||||
|
SESSION=$(grep -oP '^Session=\K.*' "$SRC")
|
||||||
|
[[ -z "$SESSION" ]] && exit 0
|
||||||
|
|
||||||
|
# Only touch Session= inside [Autologin]; other sections may have their own.
|
||||||
|
if sed -n '/^\[Autologin\]/,/^\[/p' "$DEST" | grep -q '^Session='; then
|
||||||
|
sed -i "/^\[Autologin\]/,/^\[/ s|^Session=.*|Session=$SESSION|" "$DEST"
|
||||||
|
elif grep -q '^\[Autologin\]' "$DEST"; then
|
||||||
|
sed -i "/^\[Autologin\]/a Session=$SESSION" "$DEST"
|
||||||
|
else
|
||||||
|
printf '\n[Autologin]\nSession=%s\n' "$SESSION" >> "$DEST"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 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
|
||||||
|
<name> [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` |
|
||||||
|
| `steamify-steam-autostart.service` | `lib/nvidia.sh` (Gaming on NVIDIA) | Steam at login on the Plasma desktop, normal or in Big Picture (user unit) |
|
||||||
|
| `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) |
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Steam at login (Steamify, NVIDIA PCs)
|
||||||
|
After=graphical-session.target
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# Only in the Plasma desktop session.
|
||||||
|
ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]'
|
||||||
|
ExecStart=/usr/bin/steam @ARGS@
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[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).
|
||||||
|
# Quoted: the path is in the home folder, which may contain a space.
|
||||||
|
ExecStart="@NOTIFY_SCRIPT@" --delay 60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=plasma-workspace.target
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Steamify: check for a new release daily
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+68
-3
@@ -15,11 +15,11 @@
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# Release version, see CHANGELOG.md.
|
# Release version, see CHANGELOG.md.
|
||||||
VERSION=2.3.0
|
VERSION=2.10.0
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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 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
|
# shellcheck source=/dev/null
|
||||||
source "$SCRIPT_DIR/lib/$lib.sh"
|
source "$SCRIPT_DIR/lib/$lib.sh"
|
||||||
done
|
done
|
||||||
@@ -28,8 +28,21 @@ require_root_helper
|
|||||||
|
|
||||||
BACKEND=false
|
BACKEND=false
|
||||||
[[ "${1:-}" == --backend ]] && BACKEND=true
|
[[ "${1:-}" == --backend ]] && BACKEND=true
|
||||||
|
# --defaults [--options <id>,...] [--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=false
|
||||||
|
[[ "${1:-}" == --defaults ]] && DEFAULTS=true
|
||||||
|
# --defaults --list: what --defaults can set up here, as JSON (an installer
|
||||||
|
# page is built from it); nothing else on stdout.
|
||||||
|
LIST=false
|
||||||
|
[[ "$DEFAULTS" == true && "${2:-}" == --list ]] && LIST=true
|
||||||
|
# --boot gamescope|desktop: only change where an installed conversion
|
||||||
|
# starts, everything else stays as it is (for scripts; no menu).
|
||||||
|
BOOT_ONLY=false
|
||||||
|
[[ "${1:-}" == --boot ]] && BOOT_ONLY=true
|
||||||
|
|
||||||
if [[ "$BACKEND" == false ]]; then
|
if [[ "$BACKEND" == false && "$LIST" == false ]]; then
|
||||||
echo -e "${c_bold}Steamify CachyOS${c_reset} v$VERSION"
|
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 "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."
|
echo "anything you turn off is put back the way it was."
|
||||||
@@ -43,6 +56,8 @@ fi
|
|||||||
# Everything per-user (autologin user, Steam, theme, shortcut) is for the
|
# Everything per-user (autologin user, Steam, theme, shortcut) is for the
|
||||||
# user running the script.
|
# user running the script.
|
||||||
TARGET_USER="$(id -un)"
|
TARGET_USER="$(id -un)"
|
||||||
|
migrate_layout
|
||||||
|
notify_seen
|
||||||
|
|
||||||
restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; }
|
restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; }
|
||||||
|
|
||||||
@@ -112,6 +127,55 @@ if [[ "$BACKEND" == true ]]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == --first-login ]]; then
|
||||||
|
first_login_run
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$LIST" == true ]]; then
|
||||||
|
defaults_list
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DEFAULTS" == true ]]; then
|
||||||
|
RESTART_FOR_LOGIN=false
|
||||||
|
REAPPLY=false
|
||||||
|
sudo -n true 2>/dev/null || { err "--defaults needs sudo without a password."; exit 1; }
|
||||||
|
detect_components
|
||||||
|
defaults_options "${@:2}" || exit 1
|
||||||
|
plan_changes
|
||||||
|
apply_changes
|
||||||
|
# Run by the installer: the rest waits for the first desktop login.
|
||||||
|
user_session || first_login_schedule
|
||||||
|
[[ ${#FAILED[@]} -eq 0 ]] || { warn "These had problems (see above): ${FAILED[*]}"; exit 1; }
|
||||||
|
ok "Done; the changes take effect after a restart."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$BOOT_ONLY" == true ]]; then
|
||||||
|
RESTART_FOR_LOGIN=false
|
||||||
|
REAPPLY=false
|
||||||
|
case "${2:-}" in
|
||||||
|
gamescope|desktop) ;;
|
||||||
|
*) err "--boot takes gamescope or desktop"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
detect_components
|
||||||
|
[[ "${CURRENT[gaming]}" == 1 ]] || { err "--boot needs the SteamOS conversion; turn it on first."; exit 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; [[ "$2" == desktop ]] && WANTED[boot]=1
|
||||||
|
plan_changes
|
||||||
|
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
||||||
|
ok "Already starting in $2."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
apply_changes
|
||||||
|
[[ ${#FAILED[@]} -eq 0 ]] || { warn "These had problems (see above): ${FAILED[*]}"; exit 1; }
|
||||||
|
ok "Starts in $2 from the next boot on."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Menu loop: after each run the menu comes back with the new state, until
|
# 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.
|
# the user quits; the restart question comes then, once, for everything.
|
||||||
RESTART_FOR_LOGIN=false
|
RESTART_FOR_LOGIN=false
|
||||||
@@ -125,6 +189,7 @@ while true; do
|
|||||||
plan_changes
|
plan_changes
|
||||||
|
|
||||||
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
||||||
|
feature_record_unticked
|
||||||
ok "Everything is already the way you want it."
|
ok "Everything is already the way you want it."
|
||||||
read -rp "Press Enter to go back to the menu... " _ || break
|
read -rp "Press Enter to go back to the menu... " _ || break
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Tests lib/controllers.sh (extended controller support: xone, the Xbox wireless dongle, and xpadneo, Xbox Bluetooth) with a stub pacman
|
||||||
|
# and a temp home. No root, VM or hardware needed: bash tests/controllers-test.sh
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||||
|
fail=0
|
||||||
|
check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME"
|
||||||
|
source lib/common.sh; source lib/state.sh; source lib/controllers.sh; source lib/menu.sh
|
||||||
|
# Runs as the user, and refuses anything under the real system folders: a test must never touch them.
|
||||||
|
sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; }
|
||||||
|
info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; }
|
||||||
|
kwriteconfig6() { local f="" k="" v=""; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group) shift ;; *) v="$1" ;; esac; shift; done; mkdir -p "$(dirname "$f")"; sed -i "/^$k=/d" "$f" 2>/dev/null; printf '%s=%s\n' "$k" "$v" >> "$f"; }
|
||||||
|
kreadconfig6() { local f="" k=""; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group|--default) shift ;; esac; shift; done; sed -n "s/^$k=//p" "$f" 2>/dev/null; }
|
||||||
|
# A pretend package database.
|
||||||
|
PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; PACMAN_FAIL=0
|
||||||
|
pacman() {
|
||||||
|
case "$1" in
|
||||||
|
-Q) local p; for p in "${@:2}"; do [[ " $PKGS " == *" $p "* ]] || return 1; done ;;
|
||||||
|
-S) [[ $PACMAN_FAIL == 1 ]] && return 1; echo "S ${*:4}" >> "$PACMAN_LOG"; PKGS+=" ${*:4}" ;;
|
||||||
|
-Rns) echo "R ${*:3}" >> "$PACMAN_LOG"; local p; for p in ${*:3}; do PKGS=" ${PKGS// $p / } "; done ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
HEADERS=0; install_kernel_headers() { HEADERS=$((HEADERS + 1)); }
|
||||||
|
|
||||||
|
check "off to begin with" '! extended_controller_support_status'
|
||||||
|
extended_controller_support_enable
|
||||||
|
check "installs the repo's xpadneo-dkms, xone-dkms and the dongle firmware" 'pacman -Q xpadneo-dkms xone-dkms xone-dongle-firmware'
|
||||||
|
check "kernel headers are made sure of first" '[[ $HEADERS == 1 ]]'
|
||||||
|
check "what we installed is recorded" '[[ "$(state_get extended_controller_support installed_pkgs)" == "xpadneo-dkms xone-dkms xone-dongle-firmware" ]]'
|
||||||
|
check "it is on" extended_controller_support_status
|
||||||
|
n="$(wc -l < "$PACMAN_LOG")"; extended_controller_support_enable
|
||||||
|
check "a second run installs nothing again" '[[ "$(wc -l < "$PACMAN_LOG")" == "$n" ]]'
|
||||||
|
extended_controller_support_disable
|
||||||
|
check "off: the three packages are removed again" '! pacman -Q xpadneo-dkms 2>/dev/null && ! pacman -Q xone-dkms 2>/dev/null && ! pacman -Q xone-dongle-firmware 2>/dev/null && [[ -z "$(state_get extended_controller_support installed_pkgs)" ]]'
|
||||||
|
|
||||||
|
# The AUR's xone-dkms-git conflicts with the repo's xone-dkms: it counts, and is kept.
|
||||||
|
PKGS=" xone-dkms-git xone-dongle-firmware xpadneo-dkms"; : > "$PACMAN_LOG"
|
||||||
|
check "xone-dkms-git counts as xone: already on" extended_controller_support_status
|
||||||
|
extended_controller_support_enable; extended_controller_support_disable
|
||||||
|
check "with xone-dkms-git: nothing installed and nothing removed" '[[ ! -s "$PACMAN_LOG" ]] && pacman -Q xone-dkms-git xpadneo-dkms xone-dongle-firmware'
|
||||||
|
PKGS=" xone-dkms-git"
|
||||||
|
extended_controller_support_enable
|
||||||
|
check "xone-dkms-git present: only the missing ones are installed, never xone-dkms" 'grep -q "^S xpadneo-dkms xone-dongle-firmware$" "$PACMAN_LOG" && ! pacman -Q xone-dkms 2>/dev/null'
|
||||||
|
extended_controller_support_disable
|
||||||
|
check "and off removes only those two, xone-dkms-git stays" 'pacman -Q xone-dkms-git && ! pacman -Q xpadneo-dkms 2>/dev/null'
|
||||||
|
|
||||||
|
# A failed install is an error and records nothing.
|
||||||
|
PKGS=""; PACMAN_FAIL=1
|
||||||
|
check "a failed pacman is an error" '! extended_controller_support_enable >/dev/null'
|
||||||
|
check "...and records nothing" '[[ -z "$(state_get extended_controller_support installed_pkgs)" ]]'
|
||||||
|
PACMAN_FAIL=0
|
||||||
|
|
||||||
|
# Packages that were already there are never removed by turning it off.
|
||||||
|
PKGS=" xpadneo-dkms xone-dkms xone-dongle-firmware"; : > "$PACMAN_LOG"
|
||||||
|
extended_controller_support_enable; extended_controller_support_disable
|
||||||
|
check "packages you had before stay" 'pacman -Q xpadneo-dkms xone-dkms xone-dongle-firmware && [[ ! -s "$PACMAN_LOG" ]]'
|
||||||
|
|
||||||
|
# The menu.
|
||||||
|
check "offered on every PC" 'component_available extended_controller_support'
|
||||||
|
check "not preselected: it builds kernel modules for every kernel" '[[ " ${NO_PRESELECT[*]} " == *" extended_controller_support "* ]]'
|
||||||
|
check "a top-level item with a label" '[[ -z "${PARENT[extended_controller_support]:-}" && -n "${LABEL[extended_controller_support]}" ]]'
|
||||||
|
|
||||||
|
# The "new" badge: an opt-in option added after setup shows it, but is never ticked for you.
|
||||||
|
component_selectable() { return 0; }
|
||||||
|
declare -A CURRENT=([gaming]=1 [nvidia]=0 [extended_controller_support]=0 [vram]=0 [boot]=0)
|
||||||
|
rm -f "$STATE_DIR/features.state"
|
||||||
|
check "new opt-in option (conversion on, no record): the new badge" 'feature_new_optin extended_controller_support'
|
||||||
|
check "...but it is not ticked for you (not a new default option)" '! feature_new extended_controller_support'
|
||||||
|
check "...and the menu label says (new)" '[[ "$(menu_label extended_controller_support)" == *"(new)"* ]]'
|
||||||
|
check "a new default option is still ticked for you, and is not the opt-in kind" 'feature_new vram && ! feature_new_optin vram'
|
||||||
|
check "Boot into never gets the badge (it is a choice row)" '! feature_new_optin boot'
|
||||||
|
state_set_features() { kwriteconfig6 --file "$STATE_DIR/features.state" --group State --key "$1" "$2"; }
|
||||||
|
state_set_features extended_controller_support off
|
||||||
|
check "once a run recorded it as off, the badge is gone" '! feature_new_optin extended_controller_support'
|
||||||
|
rm -f "$STATE_DIR/features.state"
|
||||||
|
CURRENT[gaming]=0; CURRENT[nvidia]=1
|
||||||
|
check "on an NVIDIA PC (Gaming on NVIDIA on, no conversion) it shows too" 'feature_new_optin extended_controller_support'
|
||||||
|
CURRENT[nvidia]=0
|
||||||
|
check "with neither the conversion nor Gaming on NVIDIA on (a fresh PC) nothing is new" '! feature_new_optin extended_controller_support && ! feature_new vram'
|
||||||
|
CURRENT[gaming]=1; CURRENT[extended_controller_support]=1
|
||||||
|
check "already on: not new" '! feature_new_optin extended_controller_support'
|
||||||
|
exit $fail
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test of the NVIDIA fix on a real PC with an NVIDIA GPU (the unit test fakes
|
||||||
|
# one: tests/nvidia-test.sh). Run from the Steamify checkout:
|
||||||
|
# bash tests/nvidia-hardware-test.sh check read-only: what is detected, what the live kernel uses, what apply would change
|
||||||
|
# bash tests/nvidia-hardware-test.sh apply really applies the fix (sudo), then reboot and run check again
|
||||||
|
# bash tests/nvidia-hardware-test.sh visual after trying gaming mode: was the picture clean? (asks you)
|
||||||
|
# Every run appends to ~/steamify-nvidia-report.txt: paste that file when asking for help.
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
# patch_file (lib/common.sh) reads the hook script from $SCRIPT_DIR/patches.
|
||||||
|
SCRIPT_DIR="$PWD"
|
||||||
|
REPORT="${NVIDIA_REPORT:-$HOME/steamify-nvidia-report.txt}"
|
||||||
|
source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh
|
||||||
|
mode="${1:-check}"
|
||||||
|
fail=0
|
||||||
|
say() { echo "$*" | tee -a "$REPORT"; }
|
||||||
|
pass() { say "PASS $*"; }
|
||||||
|
bad() { say "FAIL $*"; fail=1; }
|
||||||
|
note() { say " $*"; }
|
||||||
|
root_cat() { cat "$1" 2>/dev/null || sudo cat "$1" 2>/dev/null || echo "(unreadable)"; }
|
||||||
|
|
||||||
|
say ""; say "===== $(date '+%F %T') mode=$mode kernel=$(uname -r)"
|
||||||
|
|
||||||
|
report_system() {
|
||||||
|
say "-- GPU and software"
|
||||||
|
lspci -nn 2>/dev/null | grep -iE 'vga|3d|display' | while read -r l; do note "$l"; done
|
||||||
|
command -v nvidia-smi >/dev/null && note "driver: $(nvidia-smi --query-gpu=name,driver_version --format=csv,noheader 2>&1 | head -n 1)"
|
||||||
|
for p in nvidia-open nvidia-utils nvidia-dkms gamescope gamescope-session-cachyos linux-cachyos-nvidia-open; do
|
||||||
|
v="$(pacman -Q "$p" 2>/dev/null)" && note "$v"
|
||||||
|
done
|
||||||
|
note "boot loader file: $(nvidia_boot_file || true)"
|
||||||
|
note "cmdline: $(cat /proc/cmdline)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_state() {
|
||||||
|
say "-- Steamify's view"
|
||||||
|
if nvidia_present; then pass "a supported NVIDIA GPU (RTX 20 series or newer) with its driver is detected"
|
||||||
|
elif nvidia_legacy_gpu; then bad "the NVIDIA GPU is older than RTX 20 (on chwd's legacy lists in $VRAM_CHWD_IDS): the fix is not applied"
|
||||||
|
else bad "no NVIDIA GPU with its driver detected: the fix does nothing here"; fi
|
||||||
|
for c in /sys/class/drm/card[0-9]*/device; do
|
||||||
|
[[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] && note "NVIDIA PCI device id $(cat "$c/device") $(vram_nvidia_legacy_id "$(cat "$c/device")" && echo '(on a chwd legacy list)' || echo '(not on a legacy list: RTX 20 or newer)')"
|
||||||
|
done
|
||||||
|
ls "$VRAM_CHWD_IDS"/nvidia-*.ids >/dev/null 2>&1 || note "no chwd legacy lists found: every NVIDIA card counts as supported"
|
||||||
|
local p
|
||||||
|
for p in $NVIDIA_PARAMS; do
|
||||||
|
if grep -qw -- "$p" /proc/cmdline; then pass "$p is on the running kernel's command line"
|
||||||
|
elif nvidia_param_set "$p"; then bad "$p is in the boot loader's file but not booted yet: reboot"
|
||||||
|
else bad "$p is missing (run: bash tests/nvidia-hardware-test.sh apply)"; fi
|
||||||
|
done
|
||||||
|
say "-- the live NVIDIA driver"
|
||||||
|
local m
|
||||||
|
for m in modeset fbdev; do
|
||||||
|
v="$(root_cat /sys/module/nvidia_drm/parameters/$m)"
|
||||||
|
if [[ "$v" == Y ]]; then pass "nvidia_drm $m = Y"; else bad "nvidia_drm $m = $v (want Y)"; fi
|
||||||
|
done
|
||||||
|
lsmod | grep -q '^nvidia_drm' && pass "nvidia_drm is loaded" || bad "nvidia_drm is not loaded"
|
||||||
|
local c found=0
|
||||||
|
for c in /sys/class/drm/card[0-9]*/device; do
|
||||||
|
[[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] || continue
|
||||||
|
found=1; note "NVIDIA card: ${c%/device} driver=$(basename "$(readlink -f "$c/driver" 2>/dev/null)")"
|
||||||
|
for s in "${c%/device}"/card*-*/status; do [[ -f "$s" ]] && note " $(basename "$(dirname "$s")"): $(cat "$s")"; done
|
||||||
|
done
|
||||||
|
[[ $found == 1 ]] || bad "no NVIDIA card in /sys/class/drm"
|
||||||
|
say "-- initramfs"
|
||||||
|
if nvidia_modules_everywhere; then note "every installed kernel has nvidia_drm: the early-load drop-in is used"
|
||||||
|
else note "not every installed kernel has nvidia_drm: the early-load drop-in is skipped (parameters still apply)"; fi
|
||||||
|
nvidia_initramfs_ok && note "NVIDIA modules are in the initramfs config" || note "NVIDIA modules are not in the initramfs config"
|
||||||
|
say "-- what apply would change"
|
||||||
|
if nvidia_params_missing; then note "adds $NVIDIA_PARAMS to $(nvidia_boot_file)"; else note "kernel parameters: nothing to change"; fi
|
||||||
|
if ! nvidia_initramfs_ok && nvidia_modules_everywhere; then note "writes $NVIDIA_INITRAMFS_CONF"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
check)
|
||||||
|
report_system; check_state ;;
|
||||||
|
apply)
|
||||||
|
report_system
|
||||||
|
say "-- applying"
|
||||||
|
if nvidia_enable 2>&1 | tee -a "$REPORT"; [[ ${PIPESTATUS[0]} == 0 ]]; then pass "nvidia_enable finished"
|
||||||
|
else bad "nvidia_enable failed (output above)"; fi
|
||||||
|
say "Now reboot, then run: bash tests/nvidia-hardware-test.sh check" ;;
|
||||||
|
visual)
|
||||||
|
report_system
|
||||||
|
say "-- try gaming mode now (log in to it or reboot into it), look at the screen, then answer"
|
||||||
|
read -rp "Is the gaming mode picture clean (no corruption, no flicker)? [y/n/partly] " a
|
||||||
|
read -rp "What did you see (one line, e.g. 'garbled colours at start, fine after login')? " what
|
||||||
|
say "visual result: $a: $what"
|
||||||
|
[[ "$a" == y ]] && pass "picture clean" || bad "picture not clean ($a)"
|
||||||
|
say "-- gamescope and NVIDIA lines from this boot's log"
|
||||||
|
{ journalctl -b --no-pager 2>/dev/null || sudo journalctl -b --no-pager 2>/dev/null; } |
|
||||||
|
grep -iE 'gamescope|nvidia|drm|nvrm|wlserver|vulkan' | tail -n 80 | tee -a "$REPORT" | tail -n 5
|
||||||
|
note "(more in $REPORT)" ;;
|
||||||
|
*) echo "usage: $0 check|apply|visual"; exit 2 ;;
|
||||||
|
esac
|
||||||
|
say ""; say "Report: $REPORT"
|
||||||
|
exit $fail
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Tests lib/nvidia.sh ("Gaming on NVIDIA" and its Big Picture option) and the menu rules around it, against a fake RTX 5080
|
||||||
|
# (PCI 10de:2c02) beside an Intel iGPU, a stub modinfo, pacman and systemctl, and a temp home. No root, VM or NVIDIA
|
||||||
|
# hardware needed: bash tests/nvidia-test.sh
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||||
|
fail=0
|
||||||
|
check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME"
|
||||||
|
source lib/common.sh; source lib/state.sh; source lib/login-manager.sh; source lib/single-user.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; source lib/menu.sh
|
||||||
|
# Runs as the user, and refuses anything under the real system folders: a test must never touch them.
|
||||||
|
sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; }
|
||||||
|
info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; }
|
||||||
|
# KDE's config tools on plain files under the temp home (a relative file name is relative to ~/.config, as in KDE).
|
||||||
|
kwriteconfig6() {
|
||||||
|
local f="" k="" v="" del=0
|
||||||
|
while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group) shift ;; --delete) del=1 ;; *) v="$1" ;; esac; shift; done
|
||||||
|
[[ "$f" == /* ]] || f="$HOME/.config/$f"; mkdir -p "$(dirname "$f")"
|
||||||
|
if [[ $del == 1 ]]; then sed -i "/^$k=/d" "$f" 2>/dev/null; else sed -i "/^$k=/d" "$f" 2>/dev/null; printf '%s=%s\n' "$k" "$v" >> "$f"; fi
|
||||||
|
}
|
||||||
|
kreadconfig6() {
|
||||||
|
local f="" k="" d="" r
|
||||||
|
while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --default) d="$2"; shift ;; --group) shift ;; esac; shift; done
|
||||||
|
[[ "$f" == /* ]] || f="$HOME/.config/$f"
|
||||||
|
r="$(sed -n "s/^$k=//p" "$f" 2>/dev/null)"; printf '%s\n' "${r:-$d}"
|
||||||
|
}
|
||||||
|
# A pretend package database and systemd: what the tests assert on.
|
||||||
|
PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; ENABLED=0
|
||||||
|
pacman() {
|
||||||
|
case "$1" in
|
||||||
|
-Q) [[ " $PKGS " == *" $2 "* ]] ;;
|
||||||
|
-S) echo "S $*" >> "$PACMAN_LOG"; PKGS+=" ${*: -1}" ;;
|
||||||
|
-Rns) echo "R $*" >> "$PACMAN_LOG"; PKGS=" ${PKGS//${*: -1}/} " ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
user_systemctl() {
|
||||||
|
case "$1" in
|
||||||
|
enable) ENABLED=1 ;;
|
||||||
|
disable) ENABLED=0 ;;
|
||||||
|
is-enabled) [[ $ENABLED == 1 ]] ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
FAKE_DRIVER=1; modinfo() { [[ "$FAKE_DRIVER" == 1 ]]; }
|
||||||
|
GAMING_ON=0; gaming_status() { [[ "$GAMING_ON" == 1 ]]; }
|
||||||
|
UNIT="$HOME/.config/systemd/user/$NVIDIA_UNIT"
|
||||||
|
|
||||||
|
mkdir -p "$T/drm/card0/device" "$T/drm/card1/device"
|
||||||
|
echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device/class"
|
||||||
|
echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class"
|
||||||
|
echo 0x2c02 > "$T/drm/card1/device/device"
|
||||||
|
NVIDIA_DRM_DIR="$T/drm"
|
||||||
|
backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; }
|
||||||
|
REBUILDS=0; nvidia_rebuild_boot() { REBUILDS=$((REBUILDS + 1)); }
|
||||||
|
: > "$T/cmdline"; NVIDIA_CMDLINE="$T/cmdline" # the running kernel's command line: without the parameters
|
||||||
|
NVIDIA_INITRAMFS_CONF="$T/90-steamify-nvidia.conf"
|
||||||
|
NVIDIA_SCRIPT="$T/libexec/steamify-nvidia-initramfs"; NVIDIA_HOOK="$T/hooks/85-steamify-nvidia-initramfs.hook"
|
||||||
|
mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst"; : > "$T/mods/6.1.0-cachyos/pkgbase"
|
||||||
|
NVIDIA_MODULES_DIR="$T/mods"
|
||||||
|
NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked
|
||||||
|
mkdir -p "$T/chwd"; VRAM_CHWD_IDS="$T/chwd"
|
||||||
|
# The component's kernel step edits this boot loader file (systemd-boot's) in the tests, never a real one.
|
||||||
|
printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot-manage.conf"; nvidia_boot_file() { echo "$T/sdboot-manage.conf"; }
|
||||||
|
|
||||||
|
# --- detection
|
||||||
|
check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present
|
||||||
|
FAKE_DRIVER=0; check "no nvidia_drm module (nouveau): not an NVIDIA PC" '! nvidia_present'
|
||||||
|
FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA GPU: not an NVIDIA PC" '! nvidia_present'
|
||||||
|
NVIDIA_DRM_DIR="$T/drm"
|
||||||
|
|
||||||
|
# --- which items the menu shows
|
||||||
|
check "NVIDIA PC: the SteamOS conversion and its gamescope options are hidden" '! component_available gaming && ! component_available boot && ! component_available glyphs'
|
||||||
|
check "NVIDIA PC: Gaming on NVIDIA and Big Picture are shown" 'component_available nvidia && component_available bigpicture'
|
||||||
|
GAMING_ON=1
|
||||||
|
check "NVIDIA PC with the conversion already on: it is shown, so it can be turned off" 'component_available gaming && component_available boot'
|
||||||
|
check "...and Gaming on NVIDIA is hidden (both would start Steam)" '! component_available nvidia && ! component_available bigpicture'
|
||||||
|
GAMING_ON=0; NVIDIA_DRM_DIR="$T/none"
|
||||||
|
check "no NVIDIA: the conversion is shown as before" 'component_available gaming && component_available boot'
|
||||||
|
check "...and Gaming on NVIDIA is hidden" '! component_available nvidia && ! component_available bigpicture'
|
||||||
|
NVIDIA_DRM_DIR="$T/drm"
|
||||||
|
|
||||||
|
# --- "Add as non-Steam game" is for gaming mode's controller: not offered on an NVIDIA PC (unless already on)
|
||||||
|
steamgame_available() { return 0; }; STEAMGAME_ON=0; steamgame_status() { [[ "$STEAMGAME_ON" == 1 ]]; }
|
||||||
|
check "NVIDIA PC: Add as non-Steam game is hidden" '! component_available steamgame'
|
||||||
|
STEAMGAME_ON=1; check "...unless it is already on (so it can be turned off)" 'component_available steamgame'; STEAMGAME_ON=0
|
||||||
|
NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA: it is offered as before" 'component_available steamgame'
|
||||||
|
steamgame_available() { return 1; }; check "no NVIDIA, no Steam account yet: still not offered" '! component_available steamgame'
|
||||||
|
steamgame_available() { return 0; }; NVIDIA_DRM_DIR="$T/drm"
|
||||||
|
|
||||||
|
# --- turning it on
|
||||||
|
check "off to begin with" '! nvidia_status && ! bigpicture_status'
|
||||||
|
nvidia_enable
|
||||||
|
check "Steam is installed when missing, and recorded as ours" 'grep -q "^S " "$PACMAN_LOG" && [[ "$(state_get nvidia installed_pkgs)" == steam ]]'
|
||||||
|
check "the autostart unit is written: Steam without arguments" 'grep -q "^ExecStart=/usr/bin/steam $" "$UNIT"'
|
||||||
|
check "it is on" 'nvidia_status && ! bigpicture_status'
|
||||||
|
n="$(wc -l < "$PACMAN_LOG")"; nvidia_enable
|
||||||
|
check "a second run installs nothing again" '[[ "$(wc -l < "$PACMAN_LOG")" == "$n" ]]'
|
||||||
|
|
||||||
|
# --- Big Picture
|
||||||
|
bigpicture_enable
|
||||||
|
check "Big Picture: Steam starts with -gamepadui" 'grep -q "^ExecStart=/usr/bin/steam -gamepadui$" "$UNIT" && bigpicture_status'
|
||||||
|
nvidia_enable
|
||||||
|
check "turning Gaming on NVIDIA on again keeps Big Picture" 'bigpicture_status'
|
||||||
|
check "Big Picture: Plasma starts with an empty session" '[[ "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" == emptySession ]]'
|
||||||
|
bigpicture_disable
|
||||||
|
check "Big Picture off: Steam's normal window again" '! bigpicture_status && nvidia_status'
|
||||||
|
check "...and the session setting is back as it was (not set)" '[[ -z "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" ]]'
|
||||||
|
|
||||||
|
# --- turning it off
|
||||||
|
bigpicture_enable; nvidia_disable
|
||||||
|
check "off: the unit is gone and Steam, which we installed, is removed" '! nvidia_status && [[ ! -e "$UNIT" ]] && ! pacman -Q steam && [[ -z "$(state_get nvidia installed_pkgs)" ]]'
|
||||||
|
check "Big Picture without the unit does nothing" 'bigpicture_enable && bigpicture_disable && [[ ! -e "$UNIT" ]]'
|
||||||
|
|
||||||
|
# --- Steam that was there already is never removed
|
||||||
|
PKGS=" steam"; : > "$PACMAN_LOG"
|
||||||
|
nvidia_enable; nvidia_disable
|
||||||
|
check "Steam installed before is kept, and not installed again" '[[ ! -s "$PACMAN_LOG" ]] && pacman -Q steam'
|
||||||
|
|
||||||
|
# --- kernel parameters and early modules (a supported card only: RTX 20 series or newer)
|
||||||
|
check "RTX 5080 (2c02) is supported: no chwd legacy lists" nvidia_supported
|
||||||
|
printf '1c03\n1b80\n' > "$T/chwd/nvidia-580.ids" # a GTX 1060 and a GTX 1080: chwd's 580xx legacy list
|
||||||
|
check "RTX 5080 is not on a legacy list" 'nvidia_supported && ! nvidia_legacy_gpu'
|
||||||
|
echo 0x1c03 > "$T/drm/card1/device/device"
|
||||||
|
check "GTX 1060 (older than RTX 20): no kernel setup, but still an NVIDIA PC" '! nvidia_supported && nvidia_legacy_gpu && nvidia_present'
|
||||||
|
echo 0x2c02 > "$T/drm/card1/device/device"
|
||||||
|
|
||||||
|
for loader in limine limineplus sdboot grub; do
|
||||||
|
case $loader in
|
||||||
|
limine) F="$T/limine"; printf 'KERNEL_CMDLINE[default]="quiet splash"\n' > "$F" ;;
|
||||||
|
# CachyOS writes its line as "+=" and may already have nvidia_drm.modeset=1 in it.
|
||||||
|
limineplus) F="$T/limine"; printf 'KERNEL_CMDLINE[default]+="quiet splash nvidia_drm.modeset=1"\n' > "$F" ;;
|
||||||
|
sdboot) F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet splash"\n' > "$F" ;;
|
||||||
|
grub) F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' > "$F" ;;
|
||||||
|
esac
|
||||||
|
nvidia_boot_file() { echo "$F"; }
|
||||||
|
rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; ORIG="$(cat "$F")"
|
||||||
|
nvidia_kernel_enable
|
||||||
|
check "$loader: parameters added" 'grep -q "nvidia-drm.modeset=1" "$F" && grep -q "nvidia-drm.fbdev=1" "$F"'
|
||||||
|
check "$loader: initramfs drop-in written" 'grep -q nvidia_drm "$NVIDIA_INITRAMFS_CONF"'
|
||||||
|
check "$loader: script and pacman hook installed" '[[ -x "$NVIDIA_SCRIPT" ]] && grep -q "^Exec = $NVIDIA_SCRIPT sync" "$NVIDIA_HOOK"'
|
||||||
|
check "$loader: backup made, rebuilt once" '[[ -f "$F.bak" && $REBUILDS == 1 ]]'
|
||||||
|
SUM="$(cat "$F")"; nvidia_kernel_enable
|
||||||
|
check "$loader: second run changes nothing" '[[ "$(cat "$F")" == "$SUM" && $REBUILDS == 1 ]]'
|
||||||
|
nvidia_kernel_disable
|
||||||
|
check "$loader: disable restores the file, drops the drop-in, script and hook" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" && ! -e "$NVIDIA_SCRIPT" && ! -e "$NVIDIA_HOOK" ]]'
|
||||||
|
done
|
||||||
|
|
||||||
|
# A missing patches/ file must be an error, never an empty script or hook.
|
||||||
|
( SCRIPT_DIR="$T/no-patches"; NVIDIA_SCRIPT="$T/empty/script"; NVIDIA_HOOK="$T/empty/hook"; ! nvidia_hook_install 2>/dev/null && [[ ! -s "$T/empty/script" ]] ) && echo "ok a missing patch file is an error, nothing empty installed" || { echo "FAIL a missing patch file is an error, nothing empty installed"; fail=1; }
|
||||||
|
# The pacman hook's script decides again at every kernel change.
|
||||||
|
syncit() { NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="$NVIDIA_MODULES_DIR" bash "$NVIDIA_SCRIPT" sync; }
|
||||||
|
nvidia_hook_install; rm -f "$NVIDIA_INITRAMFS_CONF"
|
||||||
|
syncit; check "hook script: every kernel has the modules -> drop-in written" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
|
||||||
|
mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"
|
||||||
|
syncit; check "hook script: a new kernel without modules -> drop-in removed" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]]'
|
||||||
|
: > "$T/mods/6.6.0-lts/kernel/nvidia-drm.ko.zst"
|
||||||
|
syncit; check "hook script: the modules arrive (DKMS built) -> drop-in back" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
|
||||||
|
rm -rf "$T/mods/6.6.0-lts"; mkdir -p "$T/mods/5.0.0-leftover"; : > "$T/mods/5.0.0-leftover/modules.dep"
|
||||||
|
syncit; check "hook script: a leftover folder of a removed kernel is ignored" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
|
||||||
|
rm -rf "$T/mods/5.0.0-leftover"; nvidia_kernel_disable
|
||||||
|
# A second kernel without the NVIDIA modules: no early-load drop-in, parameters still set.
|
||||||
|
mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F"
|
||||||
|
nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_kernel_enable
|
||||||
|
check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"'
|
||||||
|
rm -rf "$T/mods/6.6.0-lts"
|
||||||
|
# Generated boot entries without the parameters (the initramfs build failed): an error.
|
||||||
|
printf 'linux /vmlinuz root=/dev/sda1\n' > "$T/grub.cfg"; NVIDIA_GRUB_CFG="$T/grub.cfg"
|
||||||
|
F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet"\n' > "$F"; nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"
|
||||||
|
check "boot entries without the parameters: error" '! nvidia_kernel_enable >/dev/null'
|
||||||
|
NVIDIA_GRUB_CFG="$T/none/grub.cfg"
|
||||||
|
# A config file without the setting to edit: an error, not "applied".
|
||||||
|
F="$T/sdboot-manage.conf"; printf '#LINUX_OPTIONS=""\n' > "$F"; nvidia_boot_file() { echo "$F"; }; REBUILDS=0
|
||||||
|
check "no setting to edit: error, nothing rebuilt" '! nvidia_kernel_enable >/dev/null && [[ $REBUILDS == 0 ]]'
|
||||||
|
FAKE_DRIVER=0; NVIDIA_DRM_DIR="$T/drm"; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_kernel_enable
|
||||||
|
check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]'
|
||||||
|
|
||||||
|
# --- single user mode on NVIDIA: its own login step (SDDM, autologin into Plasma) without the conversion
|
||||||
|
FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/drm" # an earlier section ended without the driver
|
||||||
|
SW="$T/switches.log"; : > "$SW"; TARGET_USER=tester; SINGLE_AUTOLOGIN="$T/sddm.conf.d/zzz-steamify-autologin.conf"
|
||||||
|
switch_to_sddm() { echo sddm >> "$SW"; }; switch_to_plasmalogin() { echo plasmalogin >> "$SW"; }
|
||||||
|
remove_sddm_base_autologin() { :; } # would look at the real /etc/sddm.conf
|
||||||
|
GAMING_ON=0
|
||||||
|
check "single user mode is offered on NVIDIA without the conversion..." 'component_available single'
|
||||||
|
GAMING_ON=1; check "...and with it" 'component_available single'; GAMING_ON=0
|
||||||
|
single_login_enable
|
||||||
|
check "single user: SDDM, autologin of the user into the Plasma session" 'grep -qx "User=tester" "$SINGLE_AUTOLOGIN" && grep -qx "Session=plasma.desktop" "$SINGLE_AUTOLOGIN" && grep -qx "Relogin=true" "$SINGLE_AUTOLOGIN" && grep -qx sddm "$SW"'
|
||||||
|
c1="$(cat "$SINGLE_AUTOLOGIN")"; single_login_enable
|
||||||
|
check "single user login: a second run changes nothing" '[[ "$(cat "$SINGLE_AUTOLOGIN")" == "$c1" ]]'
|
||||||
|
single_login_disable
|
||||||
|
check "single user off: the file is gone and plasma-login-manager is the login manager again" '[[ ! -e "$SINGLE_AUTOLOGIN" ]] && grep -qx plasmalogin "$SW"'
|
||||||
|
: > "$SW"; GAMING_ON=1; single_login_enable
|
||||||
|
check "with the conversion on, single user mode leaves the login to it" '[[ ! -e "$SINGLE_AUTOLOGIN" && ! -s "$SW" ]]'
|
||||||
|
GAMING_ON=0
|
||||||
|
# --- the menu rules
|
||||||
|
declare -A WANTED=([nvidia]=1 [bigpicture]=1 [gaming]=0 [boot]=0 [single]=0 [machine]=0 [poweroff]=0 [launcher]=0 [steamgame]=0)
|
||||||
|
component_selectable() { return 0; }
|
||||||
|
toggle_component nvidia
|
||||||
|
check "unticking Gaming on NVIDIA unticks Big Picture" '[[ "${WANTED[nvidia]}" == 0 && "${WANTED[bigpicture]}" == 0 ]]'
|
||||||
|
toggle_component bigpicture
|
||||||
|
check "ticking Big Picture ticks Gaming on NVIDIA" '[[ "${WANTED[nvidia]}" == 1 && "${WANTED[bigpicture]}" == 1 ]]'
|
||||||
|
WANTED[single]=0; WANTED[gaming]=0; toggle_component single
|
||||||
|
check "ticking single user mode does not tick the conversion where it isn't offered" '[[ "${WANTED[single]}" == 1 && "${WANTED[gaming]}" == 0 ]]'
|
||||||
|
check "Big Picture follows Gaming on NVIDIA in the menu" '[[ " ${COMPONENTS[*]} " == *" nvidia bigpicture "* && "${PARENT[bigpicture]}" == nvidia ]]'
|
||||||
|
|
||||||
|
exit $fail
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
// State and logic of the app: menu -> review -> password -> applying -> done,
|
||||||
|
// and the BIOS update (check -> warning 1 -> warning 2 -> flash). The
|
||||||
|
// screens only show this and call its functions.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: app
|
||||||
|
required property var backend
|
||||||
|
required property var gamepad
|
||||||
|
|
||||||
|
property string screen: "menu" // menu review password applying done bios1 bios2
|
||||||
|
property double remoteAt: 0
|
||||||
|
property var want: ({})
|
||||||
|
property string boot: "gamescope"
|
||||||
|
property int sel: 0 // row index; rows.length = the Apply button
|
||||||
|
property bool reapply: false
|
||||||
|
property var plan: [] // [{id, action, isNew}]
|
||||||
|
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
|
||||||
|
property var failed: []
|
||||||
|
property bool restartNeeded: false
|
||||||
|
// Review & apply only when something would change; "Re-apply what's on"
|
||||||
|
// is for when nothing did.
|
||||||
|
readonly property bool canApply: computePlan().length > 0
|
||||||
|
onCanApplyChanged: if (!canApply && sel === rows.length) sel = Math.max(0, rows.length - 1)
|
||||||
|
property string runError: ""
|
||||||
|
property bool wrongPassword: false
|
||||||
|
property bool typing: false // a text field has the keys
|
||||||
|
property int doneCount: 0
|
||||||
|
property string pending: "apply" // what the password is for: apply | bios
|
||||||
|
property string sessionPassword: "" // kept between the BIOS steps only
|
||||||
|
property var biosInfo: ({})
|
||||||
|
property bool biosRun: false
|
||||||
|
property bool biosChecking: false // the invisible check before the warnings
|
||||||
|
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
|
||||||
|
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
|
||||||
|
property bool holding: false
|
||||||
|
readonly property alias log: logModel
|
||||||
|
|
||||||
|
readonly property var status: backend.status || ({})
|
||||||
|
readonly property var items: status.items || []
|
||||||
|
readonly property var rows: items.filter(function (i) { return !i.parent || want[i.parent]; })
|
||||||
|
readonly property var bios: status.bios || null
|
||||||
|
|
||||||
|
signal passwordRequested()
|
||||||
|
signal passwordRejected()
|
||||||
|
signal passwordSubmitRequested() // A/OK on the password screen
|
||||||
|
|
||||||
|
ListModel { id: logModel }
|
||||||
|
|
||||||
|
function nowOn(id) {
|
||||||
|
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function label(item) { return Texts.label(item.id, item.label); }
|
||||||
|
function biosHint() {
|
||||||
|
if (!bios) return "";
|
||||||
|
if (!bios.newest) return "Now " + bios.current + ", newest unknown (offline?)";
|
||||||
|
if (bios.current === bios.newest) return bios.current + " is up to date";
|
||||||
|
return "Now " + bios.current + ", newest " + bios.newest + " · at your own risk";
|
||||||
|
}
|
||||||
|
function syncFromStatus() {
|
||||||
|
reapply = false;
|
||||||
|
var w = {};
|
||||||
|
for (var i = 0; i < items.length; i++) w[items[i].id] = items[i].kind === "action" ? false : items[i].wanted;
|
||||||
|
want = w;
|
||||||
|
boot = nowOn("boot") ? "desktop" : "gamescope";
|
||||||
|
if (sel > rows.length) sel = 0;
|
||||||
|
}
|
||||||
|
Connections { target: backend; function onStatusChanged() { if (app.screen === "menu" || app.screen === "done") app.syncFromStatus(); } }
|
||||||
|
|
||||||
|
// --- Menu ---
|
||||||
|
function toggle(id) {
|
||||||
|
var cur = items.find(function (i) { return i.id === id; });
|
||||||
|
if (cur && cur.kind === "toggle" && cur.selectable === false) return;
|
||||||
|
var w = Object.assign({}, want);
|
||||||
|
w[id] = !w[id];
|
||||||
|
// The same rules as the terminal menu (toggle_component).
|
||||||
|
var hasGaming = items.some(function (i) { return i.id === "gaming"; });
|
||||||
|
if (id === "gaming" && !w.gaming) w.single = false;
|
||||||
|
if (id === "single" && w.single && hasGaming) w.gaming = true;
|
||||||
|
if (id === "nvidia") w.bigpicture = w.nvidia;
|
||||||
|
if (id === "bigpicture" && w.bigpicture) w.nvidia = true;
|
||||||
|
if (id === "machine") w.poweroff = w.machine;
|
||||||
|
if (id === "poweroff" && w.poweroff) w.machine = true;
|
||||||
|
if (id === "machine" && !w.machine) w.kpin = false;
|
||||||
|
if (id === "kpin" && w.kpin) w.machine = true;
|
||||||
|
if (id === "launcher" && items.some(function (i) { return i.id === "steamgame"; })) w.steamgame = w.launcher;
|
||||||
|
if (id === "steamgame" && w.steamgame) w.launcher = true;
|
||||||
|
want = w;
|
||||||
|
}
|
||||||
|
function computePlan() {
|
||||||
|
var p = [];
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var it = items[i];
|
||||||
|
if (it.kind !== "toggle") continue;
|
||||||
|
if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
|
||||||
|
if (want[it.id] && !it.on) p.push({ id: it.id, action: "on", isNew: !!it["new"] });
|
||||||
|
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
|
||||||
|
else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" });
|
||||||
|
}
|
||||||
|
var bootNow = nowOn("boot") ? "desktop" : "gamescope";
|
||||||
|
if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" });
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
function goReview(again) {
|
||||||
|
if (!again && !canApply) return;
|
||||||
|
reapply = again; plan = computePlan(); screen = "review";
|
||||||
|
}
|
||||||
|
function wantedIds() {
|
||||||
|
var ids = [];
|
||||||
|
for (var i = 0; i < items.length; i++)
|
||||||
|
if (items[i].kind === "toggle" && want[items[i].id] && (!items[i].parent || want[items[i].parent])) ids.push(items[i].id);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Password and apply ---
|
||||||
|
function resetRun() { steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear(); }
|
||||||
|
function startApply(password) {
|
||||||
|
resetRun();
|
||||||
|
var s = {};
|
||||||
|
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
|
||||||
|
steps = s;
|
||||||
|
screen = "applying";
|
||||||
|
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
|
||||||
|
}
|
||||||
|
function askPassword(forWhat) {
|
||||||
|
pending = forWhat;
|
||||||
|
if (!backend.needsPassword()) return false;
|
||||||
|
wrongPassword = false; screen = "password"; passwordRequested();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function onApplyPressed() {
|
||||||
|
if (plan.length === 0) return;
|
||||||
|
if (!askPassword("apply")) startApply("");
|
||||||
|
}
|
||||||
|
function submitPassword(p) {
|
||||||
|
if (!backend.checkPassword(p)) { wrongPassword = true; passwordRejected(); return; }
|
||||||
|
if (pending === "bios") { sessionPassword = p; biosCheck(); }
|
||||||
|
else startApply(p);
|
||||||
|
}
|
||||||
|
function passwordBack() { screen = pending === "bios" ? "menu" : "review"; }
|
||||||
|
|
||||||
|
// --- BIOS ---
|
||||||
|
function startBios() {
|
||||||
|
if (!bios || !bios.selectable) return;
|
||||||
|
biosInfo = {}; sessionPassword = "";
|
||||||
|
if (!askPassword("bios")) biosCheck();
|
||||||
|
}
|
||||||
|
function biosCheck() {
|
||||||
|
// Invisible: stay on the menu (the BIOS row says "Checking…"),
|
||||||
|
// then the first warning.
|
||||||
|
failed = []; runError = ""; logModel.clear();
|
||||||
|
plan = [{ id: "bios", action: "check" }];
|
||||||
|
biosRun = true; biosChecking = true; screen = "menu";
|
||||||
|
backend.biosPrepare(sessionPassword);
|
||||||
|
}
|
||||||
|
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||||
|
function biosContinue() { holdProgress = 0; screen = "bios2"; }
|
||||||
|
function biosFlash() {
|
||||||
|
holdProgress = 0; holding = false;
|
||||||
|
resetRun();
|
||||||
|
plan = [{ id: "bios", action: "flash" }];
|
||||||
|
steps = { bios: "wait" };
|
||||||
|
biosRun = true; screen = "applying";
|
||||||
|
backend.biosFlash(sessionPassword);
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 50; repeat: true; running: app.screen === "bios2" && app.holding
|
||||||
|
onTriggered: { app.holdProgress = Math.min(1, app.holdProgress + 0.01); if (app.holdProgress >= 1) app.biosFlash(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToMenu() { biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||||
|
|
||||||
|
// --- Backend events ---
|
||||||
|
function errorText(e) {
|
||||||
|
return e === "wrong-password" ? "The password didn't work." : (e === "start" ? "Steamify couldn't start the changes." : (e ? "sudo isn't available." : ""));
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: app.backend
|
||||||
|
function onEvent(ev) {
|
||||||
|
var s = Object.assign({}, app.steps);
|
||||||
|
if (ev.event === "start") { s[ev.id] = "run"; app.steps = s; }
|
||||||
|
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; app.steps = s; app.doneCount++; }
|
||||||
|
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
||||||
|
else if (ev.event === "bios-ready") { app.biosChecking = false; app.biosInfo = ev; app.biosFocus = 0; app.holdProgress = 0; app.screen = "bios1"; }
|
||||||
|
else if (ev.event === "finished" && app.biosChecking) {
|
||||||
|
// The check found nothing to do or a problem: say so.
|
||||||
|
app.biosChecking = false; app.sessionPassword = "";
|
||||||
|
app.failed = ev.failed || []; app.restartNeeded = false;
|
||||||
|
app.runError = ev.nothing && !ev.error ? "The BIOS is already the newest version." : app.errorText(ev.error);
|
||||||
|
app.screen = "done";
|
||||||
|
}
|
||||||
|
else if (ev.event === "finished") {
|
||||||
|
// A successful BIOS check ends with bios-ready instead, keeping
|
||||||
|
// the password for the flash.
|
||||||
|
app.sessionPassword = "";
|
||||||
|
app.failed = ev.failed || []; app.restartNeeded = !!ev.restart;
|
||||||
|
app.runError = app.errorText(ev.error);
|
||||||
|
app.screen = "done";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onRemotePressed() { app.remoteAt = Date.now(); Input.type = "remote"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Input: one set of actions for controller, keyboard and remote ---
|
||||||
|
function act(a) {
|
||||||
|
if (screen === "menu") {
|
||||||
|
if (biosChecking) return;
|
||||||
|
if (a === "up") sel = Math.max(0, sel - 1);
|
||||||
|
// The greyed-out Apply button can't be selected.
|
||||||
|
else if (a === "down") sel = Math.min(canApply ? rows.length : rows.length - 1, sel + 1);
|
||||||
|
else if (a === "accept") {
|
||||||
|
if (sel === rows.length) { goReview(false); return; }
|
||||||
|
var r = rows[sel];
|
||||||
|
if (r.kind === "action" && r.id === "bios") startBios();
|
||||||
|
else if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
|
||||||
|
else if (r.kind === "toggle") toggle(r.id);
|
||||||
|
}
|
||||||
|
else if ((a === "left" || a === "right") && sel < rows.length && rows[sel].kind === "choice")
|
||||||
|
boot = a === "left" ? "gamescope" : "desktop";
|
||||||
|
else if (a === "apply") goReview(false);
|
||||||
|
else if (a === "reapply") goReview(true);
|
||||||
|
// B/Esc/Back don't quit: Steam's desktop layout sends Esc for a
|
||||||
|
// Steam Controller's B, which should only ever go back.
|
||||||
|
else if (a === "quit") Qt.quit();
|
||||||
|
} else if (screen === "review") {
|
||||||
|
if (a === "accept" || a === "apply") onApplyPressed();
|
||||||
|
else if (a === "back") { reapply = false; screen = "menu"; }
|
||||||
|
} else if (screen === "password") {
|
||||||
|
if (a === "accept" || a === "apply") passwordSubmitRequested();
|
||||||
|
else if (a === "back") passwordBack();
|
||||||
|
} else if (screen === "bios1") {
|
||||||
|
if (a === "left") biosFocus = 0;
|
||||||
|
else if (a === "right") biosFocus = 1;
|
||||||
|
else if (a === "accept") { if (biosFocus === 1) biosContinue(); else biosCancel(); }
|
||||||
|
else if (a === "back") biosCancel();
|
||||||
|
} else if (screen === "bios2") {
|
||||||
|
if (a === "back") biosCancel();
|
||||||
|
else if (a === "hold") holding = true;
|
||||||
|
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
|
||||||
|
} else if (screen === "done") {
|
||||||
|
if (a === "accept" && restartNeeded) backend.restart();
|
||||||
|
else if (a === "back" || a === "accept") backToMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: app.gamepad
|
||||||
|
function onButton(b) {
|
||||||
|
if (b === "a_up") { if (app.screen === "bios2") app.act("release"); return; }
|
||||||
|
if (b.slice(-3) === "_up") return;
|
||||||
|
Input.type = "controller";
|
||||||
|
if (b === "a" && app.screen === "bios2") { app.act("hold"); return; }
|
||||||
|
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
|
||||||
|
if (map[b]) app.act(map[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function keyPressed(e) {
|
||||||
|
// A text field handles its own keys, except Escape and Enter.
|
||||||
|
if (typing && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter) return;
|
||||||
|
var fromRemote = Date.now() - remoteAt < 400;
|
||||||
|
if (screen === "bios2" && Input.type !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
|
||||||
|
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
|
||||||
|
}
|
||||||
|
if (!fromRemote) Input.type = Input.fromSteam ? "steam" : "keyboard";
|
||||||
|
var k = e.key;
|
||||||
|
if (k === Qt.Key_Up) act("up");
|
||||||
|
else if (k === Qt.Key_Down) act("down");
|
||||||
|
else if (k === Qt.Key_Left) act("left");
|
||||||
|
else if (k === Qt.Key_Right) act("right");
|
||||||
|
else if (k === Qt.Key_Space) act("accept");
|
||||||
|
// Enter selects, like A on a controller: Steam's desktop layout sends
|
||||||
|
// Enter for a Steam Controller's A (and Space for Y). Ctrl+Enter
|
||||||
|
// goes straight to Review & apply.
|
||||||
|
else if ((k === Qt.Key_Return || k === Qt.Key_Enter) && (e.modifiers & Qt.ControlModifier)) act("apply");
|
||||||
|
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act("accept");
|
||||||
|
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) { if (!e.isAutoRepeat) act("back"); }
|
||||||
|
else if (k === Qt.Key_R) act("reapply");
|
||||||
|
else if (k === Qt.Key_Q && (e.modifiers & Qt.ControlModifier)) act("quit");
|
||||||
|
else return;
|
||||||
|
e.accepted = true;
|
||||||
|
}
|
||||||
|
function keyReleased(e) {
|
||||||
|
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Progress: every step of the plan on the left, the log on the right.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property AppState app
|
||||||
|
readonly property bool checking: app.biosRun && app.plan.length > 0 && app.plan[0].action === "check"
|
||||||
|
Column {
|
||||||
|
x: 40; y: 32; width: 560; spacing: 18
|
||||||
|
Text { text: app.biosRun ? (checking ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"
|
||||||
|
color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||||
|
Rectangle { width: 560; height: 10; radius: 5; color: Theme.line
|
||||||
|
Rectangle { height: 10; radius: 5; color: Theme.accent; width: app.plan.length ? parent.width * Math.min(1, (app.doneCount + 0.3) / app.plan.length) : 0
|
||||||
|
Behavior on width { NumberAnimation { duration: 300 } } } }
|
||||||
|
Row { width: 560
|
||||||
|
Text { text: "Step " + Math.min(app.plan.length, app.doneCount + 1) + " of " + app.plan.length; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280 }
|
||||||
|
Text { text: "Don't turn off the PC"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
|
||||||
|
ListView {
|
||||||
|
width: 560; clip: true; spacing: 8
|
||||||
|
height: parent.parent.height - 32 - 156 - 32
|
||||||
|
model: app.plan
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
// Follow the progress: the running step stays in view.
|
||||||
|
readonly property int running: app.plan.findIndex(function (p) { return app.steps[p.id] === "run"; })
|
||||||
|
onRunningChanged: if (running >= 0) positionViewAtIndex(running, ListView.Contain)
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: step
|
||||||
|
required property var modelData
|
||||||
|
readonly property string st: app.steps[modelData.id] || "wait"
|
||||||
|
width: 560; height: 54; radius: 12; color: st === "run" ? Theme.cardSel : Theme.card
|
||||||
|
border.width: st === "run" ? 2 : 0; border.color: Theme.accent
|
||||||
|
Row { anchors.fill: parent; anchors.leftMargin: 18; anchors.rightMargin: 18; spacing: 14
|
||||||
|
Rectangle { width: 22; height: 22; radius: 11; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: step.st === "ok" ? Theme.goodBg : (step.st === "fail" ? Theme.badBg : "transparent")
|
||||||
|
border.width: step.st === "wait" || step.st === "run" ? 2 : 0; border.color: step.st === "run" ? Theme.accent : "#343f50"
|
||||||
|
Text { anchors.centerIn: parent; text: step.st === "ok" ? "✓" : (step.st === "fail" ? "!" : ""); color: step.st === "ok" ? Theme.good : Theme.bad
|
||||||
|
font.pixelSize: 13; font.weight: Font.Bold }
|
||||||
|
// Only this dot turns while the step runs: ✓ and ! never rotate
|
||||||
|
// (an animator leaves its item at whatever angle it stopped).
|
||||||
|
Item { anchors.fill: parent; visible: step.st === "run"
|
||||||
|
Rectangle { width: 6; height: 6; radius: 3; color: Theme.accent; x: parent.width / 2 - 3; y: -3 }
|
||||||
|
RotationAnimator on rotation { running: step.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } } }
|
||||||
|
Row { width: 380; anchors.verticalCenter: parent.verticalCenter; spacing: 10
|
||||||
|
// Boot into's label is its prefix already: "Boot into desktop", not "Boot into Boot into".
|
||||||
|
Text { text: step.modelData.id === "boot" ? (Texts.actions[step.modelData.action] || [])[3] + Texts.actions[step.modelData.action][0].toLowerCase()
|
||||||
|
: (Texts.actions[step.modelData.action] || [])[3] + Texts.label(step.modelData.id)
|
||||||
|
color: step.st === "wait" ? Theme.faint : Theme.textHi; font.family: Theme.body; font.pixelSize: 16; font.weight: Font.DemiBold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight
|
||||||
|
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||||
|
Badge { id: badge; update: step.modelData.action === "update"; isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter } }
|
||||||
|
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[step.st]
|
||||||
|
color: step.st === "ok" ? Theme.good : (step.st === "fail" ? Theme.bad : Theme.label); font.family: Theme.body; font.pixelSize: 13
|
||||||
|
anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
x: 632; y: 32; width: parent.width - 672; height: parent.height - 64; spacing: 10
|
||||||
|
Text { text: "DETAILS"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||||
|
Rectangle { width: parent.width; height: parent.height - 30; radius: 16; color: "#0b0e13"; border.width: 1; border.color: "#1d2531"
|
||||||
|
ListView { id: logView; anchors.fill: parent; anchors.margins: 18; clip: true; model: app.log
|
||||||
|
onCountChanged: positionViewAtEnd()
|
||||||
|
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// "Update" (a newer version of it will be applied) or "New" (a default
|
||||||
|
// option added since the last run), after an item's name.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
property bool update: false
|
||||||
|
property bool isNew: false
|
||||||
|
visible: update || isNew
|
||||||
|
height: 20
|
||||||
|
text: update ? "Update" : "New"
|
||||||
|
fg: update ? Theme.warn : Theme.good
|
||||||
|
bgc: update ? Theme.warnBg : Theme.goodBg
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// The bar at the bottom of a screen, for its buttons.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: Theme.bar
|
||||||
|
Rectangle { width: parent.width; height: 1; color: Theme.line }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// BIOS update, warning 2 (last chance): type UPDATE on a keyboard, or hold
|
||||||
|
// A/OK for 5 seconds on a controller or remote.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: screen
|
||||||
|
required property AppState app
|
||||||
|
onVisibleChanged: if (visible) { typed.text = ""; if (Input.type === "keyboard") typed.forceActiveFocus(); }
|
||||||
|
Binding { target: screen.app; property: "typing"; value: typed.activeFocus; when: screen.visible }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent; width: 760; height: col.implicitHeight + 64; radius: 20; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||||
|
Column { id: col; anchors.fill: parent; anchors.margins: 32; spacing: 18
|
||||||
|
Text { text: "Last chance: this flashes BIOS " + (app.biosInfo.newest || ""); color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 32; font.weight: Font.DemiBold }
|
||||||
|
Text { width: parent.width; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 16; lineHeight: 1.3
|
||||||
|
text: "After this, keep the power on until the machine has fully started again. Don't touch it, even if the screen is black." }
|
||||||
|
Column { visible: Input.type === "keyboard"; width: parent.width; spacing: 8
|
||||||
|
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||||
|
TextField { id: typed; width: 320; height: 50; color: Theme.textHi; font.family: Theme.mono; font.pixelSize: 20; leftPadding: 14
|
||||||
|
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
|
||||||
|
background: Rectangle { radius: 10; color: Theme.bg; border.width: 2; border.color: typed.text === "UPDATE" ? Theme.bad : "#5a3a3a" }
|
||||||
|
Keys.onReturnPressed: if (text === "UPDATE") app.biosFlash()
|
||||||
|
Keys.onEnterPressed: if (text === "UPDATE") app.biosFlash()
|
||||||
|
Keys.onEscapePressed: app.biosCancel() } }
|
||||||
|
Column { visible: Input.type !== "keyboard"; width: parent.width; spacing: 10
|
||||||
|
Text { text: "Hold " + Input.g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||||
|
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
|
||||||
|
Rectangle { height: 14; radius: 7; color: Theme.bad; width: parent.width * app.holdProgress } } }
|
||||||
|
Row { spacing: 12
|
||||||
|
Btn { k: Input.g.back; text: "Cancel"; onClicked: app.biosCancel() }
|
||||||
|
Text { visible: app.bios && app.bios.dryRun; text: "Dry run: nothing will be flashed."; color: Theme.warn; font.family: Theme.body; font.pixelSize: 14
|
||||||
|
anchors.verticalCenter: parent.verticalCenter } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// BIOS update, warning 1: the risks and what was checked; Cancel is
|
||||||
|
// selected first.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property AppState app
|
||||||
|
Rectangle {
|
||||||
|
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 24; radius: 16; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||||
|
Row { anchors.fill: parent; anchors.margins: 32; spacing: 40
|
||||||
|
Column { width: 640; spacing: 16
|
||||||
|
Row { spacing: 12
|
||||||
|
Rectangle { width: 44; height: 44; radius: 12; color: "#3a1c1f"
|
||||||
|
Text { anchors.centerIn: parent; text: "!"; color: Theme.bad; font.pixelSize: 26; font.weight: Font.Bold } }
|
||||||
|
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter } }
|
||||||
|
RiskList { width: 640 }
|
||||||
|
}
|
||||||
|
Rectangle { width: 440; height: 250; radius: 14; color: Theme.card
|
||||||
|
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
|
||||||
|
Repeater { model: [["Current BIOS", app.biosInfo.current || ""], ["New BIOS", app.biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
|
||||||
|
["Compatible", app.biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
|
||||||
|
Row { required property var modelData; width: 396
|
||||||
|
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 15; width: 150 }
|
||||||
|
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? Theme.warn : Theme.text; font.family: Theme.mono; font.pixelSize: 15
|
||||||
|
width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bar {
|
||||||
|
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"
|
||||||
|
color: Theme.soft; font.family: Theme.body; font.pixelSize: 15 }
|
||||||
|
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
|
||||||
|
Btn { k: app.biosFocus === 0 ? Input.g.ok : "◀"; text: "Cancel"; focusRing: app.biosFocus === 0; onClicked: app.biosCancel() }
|
||||||
|
Btn { k: app.biosFocus === 1 ? Input.g.ok : "▶"; text: "I understand, continue"; focusRing: app.biosFocus === 1; color: Theme.danger
|
||||||
|
onClicked: app.biosContinue() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// A button with the input's button name in front.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: btn
|
||||||
|
property string k
|
||||||
|
property string text
|
||||||
|
property bool primary: false
|
||||||
|
property bool focusRing: false
|
||||||
|
signal clicked
|
||||||
|
height: 48; radius: 12; width: row.implicitWidth + 36
|
||||||
|
color: primary ? Theme.accent : Theme.button
|
||||||
|
border.width: focusRing && enabled ? 2 : 0; border.color: Theme.textHi
|
||||||
|
// Greyed out while there's nothing to do.
|
||||||
|
opacity: enabled ? 1 : 0.4
|
||||||
|
Row { id: row; anchors.centerIn: parent; spacing: 10
|
||||||
|
Glyph { k: btn.k; visible: btn.k !== ""; dark: btn.primary; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { text: btn.text; color: btn.primary ? Theme.ink : Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
}
|
||||||
|
MouseArea { anchors.fill: parent; enabled: btn.enabled; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// A small label in capitals.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: chip
|
||||||
|
property string text
|
||||||
|
property color fg: Theme.good
|
||||||
|
property color bgc: Theme.goodBg
|
||||||
|
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
|
||||||
|
Text { id: ct; anchors.centerIn: parent; text: chip.text.toUpperCase(); color: chip.fg
|
||||||
|
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Right of the menu: what the selected item does, or a nudge to review
|
||||||
|
// when the Apply button is selected.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: detail
|
||||||
|
required property AppState app
|
||||||
|
readonly property var it: app.sel < app.rows.length ? app.rows[app.sel] : null
|
||||||
|
readonly property var tx: it ? Texts.of(it) : ({})
|
||||||
|
radius: 16; color: Theme.card
|
||||||
|
// Long texts scroll instead of running out of the card; each item
|
||||||
|
// starts at the top.
|
||||||
|
onItChanged: flick.contentY = 0
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: flick
|
||||||
|
anchors.fill: parent; anchors.margins: 24; anchors.rightMargin: 12
|
||||||
|
visible: detail.it !== null; clip: true
|
||||||
|
contentHeight: col.implicitHeight; boundsBehavior: Flickable.StopAtBounds
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
Column {
|
||||||
|
id: col; width: flick.width - 12; spacing: 14
|
||||||
|
Row { spacing: 10
|
||||||
|
Chip { readonly property var it: detail.it
|
||||||
|
text: !it ? "" : it.kind === "choice" ? (app.boot === "desktop" ? "Desktop" : "Gaming") : it.kind === "action" ? "Opt-in" : (it.on ? "On" : "Off")
|
||||||
|
fg: it && it.on ? Theme.good : Theme.label; bgc: it && it.on ? Theme.goodBg : Theme.line }
|
||||||
|
Chip { visible: !!detail.tx.experimental; text: "Experimental"; fg: Theme.warn; bgc: Theme.warnBg }
|
||||||
|
}
|
||||||
|
Text { text: detail.it ? app.label(detail.it) : ""; color: Theme.text; font.family: Theme.display; font.pixelSize: 26; font.weight: Font.DemiBold
|
||||||
|
wrapMode: Text.WordWrap; width: parent.width }
|
||||||
|
Text { text: detail.tx.body || ""; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
|
||||||
|
Text { text: "WHAT IT CHANGES"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
|
||||||
|
Repeater { model: detail.tx.changes || []
|
||||||
|
Text { required property string modelData; text: "• " + modelData; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||||
|
wrapMode: Text.WordWrap; width: col.width } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent; spacing: 8; visible: detail.it === null
|
||||||
|
Text { text: app.computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: Theme.text
|
||||||
|
font.family: Theme.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
|
||||||
|
Text { text: "then apply it"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// The result of a run: all done, a problem, or nothing changed. With a
|
||||||
|
// problem, the run's output is shown below it (a BIOS check has no progress
|
||||||
|
// screen, so this is the only place to read why it stopped).
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property AppState app
|
||||||
|
readonly property bool problem: app.failed.length > 0 || app.runError !== ""
|
||||||
|
readonly property string message: {
|
||||||
|
if (app.runError) return app.runError;
|
||||||
|
if (!app.biosRun) return app.plan.length + (app.plan.length === 1 ? " change" : " changes") + " applied." + (app.restartNeeded ? " Some take effect after a restart." : "");
|
||||||
|
if (app.failed.length) return "The BIOS was not changed.";
|
||||||
|
return (app.bios && app.bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (app.biosInfo.newest || "") + " is staged and is written during the restart. ")
|
||||||
|
+ "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.";
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent; width: 760; spacing: 20
|
||||||
|
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter; color: problem ? Theme.warnBg : Theme.goodBg
|
||||||
|
Text { anchors.centerIn: parent; text: problem ? "!" : "✓"; color: problem ? Theme.warn : Theme.good; font.pixelSize: 36; font.weight: Font.Bold } }
|
||||||
|
Text { text: app.runError ? "Nothing changed" : (app.biosRun && app.failed.length ? "The BIOS update stopped" : (app.failed.length ? "Done, with a problem" : "All done"))
|
||||||
|
color: Theme.text; font.family: Theme.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
|
||||||
|
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 17; text: message }
|
||||||
|
Rectangle { visible: app.failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: Theme.warnBg
|
||||||
|
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: Theme.body; font.pixelSize: 14
|
||||||
|
text: "Had a problem: " + app.failed.map(function (id) { return Texts.label(id); }).join(", ") + (app.log.count ? ". Details below." : ".") } }
|
||||||
|
Rectangle { visible: app.failed.length > 0 && app.log.count > 0; width: parent.width; height: 190; radius: 14; color: Theme.card
|
||||||
|
ListView { id: details; anchors.fill: parent; anchors.margins: 16; clip: true; model: app.log
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
// The end is where it stopped.
|
||||||
|
onCountChanged: positionViewAtEnd()
|
||||||
|
onVisibleChanged: if (visible) positionViewAtEnd()
|
||||||
|
delegate: Text { required property string line; width: details.width - 12; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
|
||||||
|
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
|
||||||
|
Btn { k: Input.g.back; text: "Back to the menu"; height: 52; onClicked: app.act("back") }
|
||||||
|
Btn { visible: app.restartNeeded; k: Input.g.ok; text: "Restart now"; primary: true; height: 52; onClicked: app.backend.restart() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// A button name (A, Enter, OK...), round for a controller's face buttons.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: glyph
|
||||||
|
property string k
|
||||||
|
property bool dark: false
|
||||||
|
height: 26; width: Math.max(26, gt.implicitWidth + 14)
|
||||||
|
radius: (Input.type === "controller" || Input.type === "steam") && k.length === 1 ? 13 : 7
|
||||||
|
color: dark ? Theme.ink : Theme.key
|
||||||
|
Text { id: gt; anchors.centerIn: parent; text: glyph.k; color: glyph.dark ? Theme.accent : Theme.textHi
|
||||||
|
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Logo, the screen's name (the version on the menu) and the machine.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property AppState app
|
||||||
|
required property string iconUrl
|
||||||
|
readonly property var titles: ({ review: "Review", password: "Password", applying: app.biosRun ? "BIOS update" : "Applying",
|
||||||
|
done: "Done", bios1: "BIOS update", bios2: "BIOS update" })
|
||||||
|
width: parent.width; height: 72; color: Theme.bar
|
||||||
|
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.line }
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
|
||||||
|
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { text: "Steamify"; color: Theme.text; font.family: Theme.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { readonly property bool menu: app.screen === "menu"
|
||||||
|
text: menu ? "v" + (app.status.version || "") : "/ " + titles[app.screen]
|
||||||
|
color: Theme.faint; font.family: menu ? Theme.mono : Theme.body; font.pixelSize: menu ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
visible: app.status.steamMachine === true
|
||||||
|
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: 34; radius: 17; color: "#1b2330"; width: mt.implicitWidth + 28
|
||||||
|
Text { id: mt; anchors.centerIn: parent; text: "Steam Machine · CachyOS"; color: Theme.label; font.family: Theme.body; font.pixelSize: 14 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// The input in use (controller, keyboard or remote) and the button names
|
||||||
|
// shown for it.
|
||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property string type: "keyboard"
|
||||||
|
// Started from Steam: keys come from a Steam Controller through Steam's
|
||||||
|
// desktop layout (A = Enter, B = Esc, Y = Space, X = Steam's keyboard),
|
||||||
|
// shown as "steam" with the controller's buttons.
|
||||||
|
property bool fromSteam: false
|
||||||
|
readonly property var glyphs: ({
|
||||||
|
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "", apply: "X", ok: "A", back: "B" },
|
||||||
|
keyboard: { toggle: "Enter", choose: "← →", reapply: "R", quit: "Ctrl+Q", apply: "Ctrl+Enter", ok: "Enter", back: "Esc" },
|
||||||
|
steam: { toggle: "A", choose: "◀ ▶", reapply: "", quit: "", apply: "↓ A", ok: "A", back: "B" },
|
||||||
|
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "", apply: "↓ Apply", ok: "OK", back: "Back" }
|
||||||
|
})
|
||||||
|
readonly property var g: glyphs[type]
|
||||||
|
}
|
||||||
+26
-1110
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
|||||||
|
// One menu item: name, badge and hint on the left; its control (switch,
|
||||||
|
// Gaming/Desktop choice or action button) on the right. Sub-items fold in
|
||||||
|
// and out with their parent.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: row
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
required property AppState app
|
||||||
|
readonly property var it: modelData
|
||||||
|
readonly property bool shown: !it.parent || !!app.want[it.parent]
|
||||||
|
readonly property int rowIndex: app.rows.findIndex(function (r) { return r.id === it.id; })
|
||||||
|
readonly property bool selected: shown && rowIndex === app.sel
|
||||||
|
readonly property bool on: !!app.want[it.id]
|
||||||
|
// Greyed out: an action with nothing to do, or an option this machine
|
||||||
|
// can't turn on.
|
||||||
|
readonly property bool unavailable: (it.kind === "action" && !(app.bios && app.bios.selectable)) || (it.kind === "toggle" && it.selectable === false)
|
||||||
|
readonly property string hint: it.id === "bios" ? app.biosHint()
|
||||||
|
: it.selectable === false ? it.hint
|
||||||
|
: ((Texts.items[it.id] || {}).hint || it.hint)
|
||||||
|
|
||||||
|
height: shown ? 62 : 0
|
||||||
|
opacity: shown ? 1 : 0
|
||||||
|
clip: true
|
||||||
|
Behavior on height { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width; height: 56; radius: 12
|
||||||
|
color: row.selected ? Theme.cardSel : Theme.card
|
||||||
|
border.width: row.selected ? 2 : 0; border.color: Theme.accent
|
||||||
|
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: { Input.type = "keyboard"; if (row.selected) app.act("accept"); else app.sel = row.rowIndex; } }
|
||||||
|
|
||||||
|
Text { visible: !!row.it.parent; x: 20; anchors.verticalCenter: parent.verticalCenter; text: "└"; color: "#56627a"; font.pixelSize: 18 }
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter; spacing: 2
|
||||||
|
x: row.it.parent ? 46 : 16
|
||||||
|
width: controls.x - x - 16
|
||||||
|
opacity: row.unavailable ? 0.6 : 1
|
||||||
|
Row { width: parent.width; spacing: 10
|
||||||
|
Text { text: app.label(row.it); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight
|
||||||
|
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||||
|
Badge { id: badge; update: !!row.it.update; isNew: !!row.it["new"]; anchors.verticalCenter: parent.verticalCenter } }
|
||||||
|
Text { text: row.hint; color: Theme.mute; font.family: Theme.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every control ends on the same edge.
|
||||||
|
Item {
|
||||||
|
id: controls
|
||||||
|
opacity: row.it.kind === "toggle" && row.it.selectable === false ? 0.4 : 1
|
||||||
|
anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 180; height: 32
|
||||||
|
// choice
|
||||||
|
Rectangle { visible: row.it.kind === "choice"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 172; height: 32; radius: 10; color: Theme.bg
|
||||||
|
Row { anchors.centerIn: parent; spacing: 4
|
||||||
|
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
|
||||||
|
Rectangle { required property var modelData
|
||||||
|
readonly property bool picked: app.boot === modelData[0]
|
||||||
|
width: 80; height: 26; radius: 7; color: picked ? Theme.accent : "transparent"
|
||||||
|
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: parent.picked ? Theme.ink : Theme.mute
|
||||||
|
font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
||||||
|
MouseArea { anchors.fill: parent; onClicked: app.boot = parent.modelData[0] } } } } }
|
||||||
|
// toggle: "now on/off" left of the switch
|
||||||
|
Rectangle { id: sw; visible: row.it.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 46; height: 26; radius: 13; color: row.on ? Theme.accent : "#343f50"
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
|
||||||
|
Text { visible: row.it.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: row.it.on ? "now on" : "now off"; color: row.it.on ? Theme.good : Theme.faint; font.family: Theme.mono; font.pixelSize: 12 }
|
||||||
|
// action (Update BIOS)
|
||||||
|
Rectangle { visible: row.it.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: at.implicitWidth + 24; height: 30; radius: 8; color: Theme.warnBg
|
||||||
|
Text { id: at; anchors.centerIn: parent; color: Theme.warn; font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold
|
||||||
|
text: app.biosChecking ? "Checking…" : app.bios && app.bios.selectable ? "Update…" : (app.bios && app.bios.newest ? "Up to date" : "Unavailable") } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// The menu: every item on the left, what the selected one does and the
|
||||||
|
// system on the right, Review & apply at the bottom.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: screen
|
||||||
|
required property AppState app
|
||||||
|
readonly property var g: Input.g
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: app.items.length === 0
|
||||||
|
anchors.centerIn: parent; color: Theme.mute; font.family: Theme.body; font.pixelSize: 18
|
||||||
|
text: app.status.error ? "Couldn't read the current state:\n" + app.status.error : "Checking what's on…"
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: app.items.length > 0
|
||||||
|
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 20
|
||||||
|
Column {
|
||||||
|
width: 740; height: parent.height; spacing: 10
|
||||||
|
Text { text: "What do you want?"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||||
|
ListView {
|
||||||
|
id: list; width: parent.width; height: parent.height - 50; clip: true
|
||||||
|
// A fixed model: sub-items fold in and out instead of the
|
||||||
|
// list being rebuilt (and jumping) on every tick.
|
||||||
|
model: app.items
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
delegate: MenuRow {
|
||||||
|
app: screen.app
|
||||||
|
width: list.width - 12; x: 2
|
||||||
|
onSelectedChanged: if (selected) Qt.callLater(list.positionViewAtIndex, index, ListView.Contain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
|
||||||
|
DetailPanel { app: screen.app; width: parent.width; height: parent.height - sys.height - 16 }
|
||||||
|
Rectangle {
|
||||||
|
id: sys; width: parent.width; height: 82; radius: 16; color: Theme.card
|
||||||
|
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
|
||||||
|
Repeater {
|
||||||
|
model: [["Kernel", (app.status.kernel || "").replace("-cachyos", "")],
|
||||||
|
["LED bar", app.status.steamMachine ? (app.status.leds || 0) + " LEDs" : "—"],
|
||||||
|
["HDMI-CEC", app.status.cecDevices ? "/dev/" + app.status.cecDevices.split(" ")[0] : "none"],
|
||||||
|
["Version", app.status.version || ""]]
|
||||||
|
Row { required property var modelData; width: (sys.width - 60) / 2
|
||||||
|
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 13; width: parent.width / 2 }
|
||||||
|
Text { text: parent.modelData[1]; color: Theme.text; font.family: Theme.mono; font.pixelSize: 13; width: parent.width / 2
|
||||||
|
horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bar {
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
|
||||||
|
Repeater {
|
||||||
|
// Quit only has a key on a keyboard (Ctrl+Q), and re-apply has
|
||||||
|
// none through Steam's layout; a controller quits through Steam.
|
||||||
|
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
|
||||||
|
.filter(function (h) { return h[0] !== ""; })
|
||||||
|
Row { required property var modelData; spacing: 8
|
||||||
|
Glyph { k: parent.modelData[0]; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { text: parent.modelData[1]; color: Theme.label; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
||||||
|
Text { readonly property int n: app.computePlan().length
|
||||||
|
text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")
|
||||||
|
color: Theme.faint; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Btn { k: screen.g.apply; text: "Review & apply"; primary: true; enabled: app.canApply
|
||||||
|
focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// The sudo password, asked once per run; never stored.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: screen
|
||||||
|
required property AppState app
|
||||||
|
color: "#0c1016"
|
||||||
|
function submit() { var p = pw.text; pw.text = ""; app.submitPassword(p); }
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: screen.app
|
||||||
|
function onPasswordRequested() { pw.text = ""; pw.shown = false; pw.forceActiveFocus(); }
|
||||||
|
function onPasswordRejected() { pw.forceActiveFocus(); }
|
||||||
|
function onPasswordSubmitRequested() { screen.submit(); }
|
||||||
|
}
|
||||||
|
Binding { target: screen.app; property: "typing"; value: pw.activeFocus; when: screen.visible }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent; width: 560; height: col.implicitHeight + 72; radius: 20; color: Theme.card; border.width: 1; border.color: Theme.line
|
||||||
|
Column {
|
||||||
|
id: col; anchors.fill: parent; anchors.margins: 36; anchors.leftMargin: 40; anchors.rightMargin: 40; spacing: 18
|
||||||
|
Row { spacing: 14
|
||||||
|
Rectangle { width: 48; height: 48; radius: 14; color: Theme.infoBg
|
||||||
|
// A padlock.
|
||||||
|
Canvas { anchors.centerIn: parent; width: 24; height: 24
|
||||||
|
onPaint: { var c = getContext("2d"); c.strokeStyle = Theme.info; c.lineWidth = 1.8; c.beginPath(); c.roundedRect(4.5, 10.5, 15, 10, 2.5, 2.5); c.stroke();
|
||||||
|
c.beginPath(); c.moveTo(8, 10.5); c.lineTo(8, 8); c.arc(12, 8, 4, Math.PI, 0); c.lineTo(16, 10.5); c.stroke(); } } }
|
||||||
|
Column { spacing: 2; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
Text { text: "Your password, once"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||||
|
Text { text: "Steamify needs it to change system settings."; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14 } }
|
||||||
|
}
|
||||||
|
Text { text: "Password for " + app.backend.user(); color: Theme.soft; font.family: Theme.body; font.pixelSize: 14; font.weight: Font.DemiBold }
|
||||||
|
TextField {
|
||||||
|
id: pw; width: parent.width; height: 52; echoMode: shown ? TextInput.Normal : TextInput.Password
|
||||||
|
property bool shown: false
|
||||||
|
color: Theme.textHi; font.family: Theme.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
|
||||||
|
placeholderText: "Password"; placeholderTextColor: "#6b778a"
|
||||||
|
background: Rectangle { radius: 12; color: Theme.bg; border.width: 2; border.color: app.wrongPassword ? Theme.bad : Theme.accent }
|
||||||
|
Keys.onReturnPressed: screen.submit()
|
||||||
|
Keys.onEnterPressed: screen.submit()
|
||||||
|
Keys.onEscapePressed: app.act("back")
|
||||||
|
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"
|
||||||
|
color: Theme.label; font.family: Theme.body; font.pixelSize: 13
|
||||||
|
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
|
||||||
|
}
|
||||||
|
Text { visible: app.wrongPassword; text: "That password didn't work. Try again."; color: Theme.bad; font.family: Theme.body; font.pixelSize: 14 }
|
||||||
|
Rectangle { width: parent.width; height: 46; radius: 12; color: Theme.note
|
||||||
|
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||||
|
text: ({ controller: "Press Steam + X for the on-screen keyboard.", keyboard: "Type your password and press Enter.", steam: "Press X for Steam's on-screen keyboard, type your password and press A.", remote: "Select the field for the on-screen keyboard." })[Input.type] } }
|
||||||
|
Item { width: parent.width; height: 48
|
||||||
|
Btn { k: Input.g.back; text: "Back"; onClicked: app.act("back") }
|
||||||
|
Btn { anchors.right: parent.right; k: Input.g.ok; text: "Apply"; primary: true; onClicked: screen.submit() } }
|
||||||
|
Text { text: "Only used for this run. Steamify never stores it."; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// What a run will change, with a summary; Apply asks for the password.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls.Basic
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property AppState app
|
||||||
|
Column {
|
||||||
|
x: 40; y: 32; width: 740; spacing: 14
|
||||||
|
Text { text: app.plan.length === 0 ? "Nothing to change" : "This will"; color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||||
|
ListView {
|
||||||
|
width: 740; clip: true; spacing: 8
|
||||||
|
height: Math.min(contentHeight, parent.parent.height - 32 - 50 - 14 - 66 - 64 - 32)
|
||||||
|
model: app.plan
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: step
|
||||||
|
required property var modelData
|
||||||
|
readonly property var st: Texts.actions[modelData.action] || ["", Theme.text, Theme.card]
|
||||||
|
width: 740; height: 56; radius: 12; color: Theme.card
|
||||||
|
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
|
||||||
|
Chip { text: step.st[0]; fg: step.st[1]; bgc: step.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { text: Texts.label(step.modelData.id); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Badge { isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle { width: 740; height: 52; radius: 12; color: Theme.note
|
||||||
|
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."
|
||||||
|
color: Theme.soft; font.family: Theme.body; font.pixelSize: 14 } }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: Theme.card
|
||||||
|
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
|
||||||
|
Text { text: "SUMMARY"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||||
|
Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]]
|
||||||
|
Row { required property var modelData; width: 380
|
||||||
|
Text { text: parent.modelData[0]; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; width: 300 }
|
||||||
|
Text { readonly property string action: parent.modelData[1]
|
||||||
|
text: app.plan.filter(function (p) { return p.action === action; }).length
|
||||||
|
color: Theme.text; font.family: Theme.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bar {
|
||||||
|
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.back; text: "Back to the menu"; onClicked: app.act("back") }
|
||||||
|
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.ok; text: "Apply"; primary: true
|
||||||
|
visible: app.plan.length > 0; onClicked: app.onApplyPressed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// The BIOS update's risks.
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: list
|
||||||
|
spacing: 8
|
||||||
|
Repeater {
|
||||||
|
model: ["A failed or interrupted BIOS update can leave the machine unable to start (bricked). Steamify, CachyOS and Valve take no responsibility for that.",
|
||||||
|
"NEVER turn off the power, unplug the machine or press the power button while it updates, including the restart(s) afterwards.",
|
||||||
|
"The screen can stay black for several minutes. Wait.",
|
||||||
|
"Only on mains power, with all other programs closed."]
|
||||||
|
Row { required property string modelData; spacing: 10; width: list.width
|
||||||
|
Rectangle { width: 6; height: 6; radius: 3; color: Theme.bad; y: 9 }
|
||||||
|
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: Theme.soft
|
||||||
|
font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.3 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Texts per item; the backend's labels are the fallback.
|
||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
readonly property var items: ({
|
||||||
|
gaming: { label: "SteamOS conversion", hint: "Boot into gaming mode, Steam on the desktop",
|
||||||
|
body: "Boots straight into gaming mode. Switch to Desktop in Steam works, and Return to Gaming Mode on the desktop brings you back.",
|
||||||
|
changes: ["Autologin into gamescope", "Return to Gaming Mode shortcut", "Steam starts silently on the desktop"] },
|
||||||
|
boot: { label: "Boot into", hint: "Where the PC starts",
|
||||||
|
body: "Where the PC starts after a restart. Switching back and forth works the same either way.",
|
||||||
|
changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] },
|
||||||
|
nvidia: { label: "Gaming on NVIDIA", hint: "Steam on the desktop, started at login",
|
||||||
|
body: "gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards (an open NVIDIA driver bug), so it isn't offered here. This puts Steam on the Plasma desktop instead and starts it when you log in.",
|
||||||
|
changes: ["Steam is installed when it's missing", "Steam starts at login (a systemd user service)", "Off: the service is removed again; Steam is only removed if Steamify installed it"] },
|
||||||
|
bigpicture: { label: "Steam starts in Big Picture", hint: "The controller-friendly Steam, or the normal window",
|
||||||
|
body: "Steam opens in Big Picture at login, which works well with a controller on the TV. Untick it for Steam's normal window.",
|
||||||
|
changes: ["Steam is started with -gamepadui", "Plasma starts with an empty session, so old windows don't cover Big Picture", "Off: Steam starts in its normal window"] },
|
||||||
|
theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam",
|
||||||
|
body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.",
|
||||||
|
changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] },
|
||||||
|
glyphs: { label: "Steam Deck/Machine icons", hint: "Deck button icons in gaming mode",
|
||||||
|
body: "Steam shows Steam Deck button icons in gaming mode.", changes: ["STEAM_GAMEPADUI_ARGS -steamos3"] },
|
||||||
|
single: { label: "Single user mode", hint: "No password, lock screen or wallet prompts",
|
||||||
|
body: "Like SteamOS: never a login or lock screen, and no KDE wallet password prompts. Typing a password with a controller is no fun.",
|
||||||
|
changes: ["SDDM autologin, no lock screen or user switching", "Valve's empty wallet; your own is kept aside", "Launcher shows Sleep, Restart, Shut Down only"] },
|
||||||
|
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"] },
|
||||||
|
steamgame: { label: "Add as non-Steam game", hint: "Steamify in your Steam library",
|
||||||
|
body: "Adds Steamify to your Steam library as a non-Steam game. Started from Steam it gets your controller (a Steam Controller only talks to Steam while Steam runs), and you can open it in gaming mode. Steam closes for a moment and starts again, since it only reads its games list at startup: so this is done from the desktop, not from gaming mode or with Steamify started from Steam.",
|
||||||
|
changes: ["Steamify CachyOS in your Steam library, with its icon", "An entry that's already there is updated, not added twice", "Off (or the shortcut off): removed from the library again"] },
|
||||||
|
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"] },
|
||||||
|
extended_controller_support: { label: "Extended controller support", hint: "Xbox wireless dongle, Xbox controllers over Bluetooth",
|
||||||
|
body: "Drivers for Xbox controllers that Linux doesn't handle well by itself. xone makes the Xbox wireless dongle work (the kernel has no driver for it), xpadneo gives an Xbox controller over Bluetooth rumble including the triggers, the right button mapping and its battery level. They are built for every installed kernel, which takes a minute or two each.",
|
||||||
|
changes: ["xone (the dongle) with its firmware, and xpadneo (Bluetooth), from the CachyOS repo", "Kernel headers for every installed kernel, so the drivers build for each", "Off: only what Steamify installed is removed again"] },
|
||||||
|
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, NVIDIA with its open kernel modules from driver 615) 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"] },
|
||||||
|
cec: { label: "HDMI-CEC", hint: "Use Steam with the TV remote, TV on/off with the PC", experimental: true,
|
||||||
|
body: "Use Steam with your TV remote, and the TV turns on and off with the PC. Turn on CEC on the TV too (Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink).",
|
||||||
|
changes: ["Valve's cecd and cec-audio-control", "HDMI CEC section in Steam's Display settings", "Volume buttons for the TV in the Quick Access menu"] },
|
||||||
|
machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam",
|
||||||
|
body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.",
|
||||||
|
changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] },
|
||||||
|
poweroff: { label: "Power-off fix", hint: "Stays off after shutting down",
|
||||||
|
body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.",
|
||||||
|
changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] },
|
||||||
|
kpin: { label: "Pin the kernel", hint: "No longer needed: untick for CachyOS's current kernel",
|
||||||
|
body: "Kept the Steam Machine on CachyOS kernel 7.1.6, which powered off properly. The power-off fix does that on every kernel now, so the pin is removed and CachyOS's current kernel comes back.",
|
||||||
|
changes: ["Removed from IgnorePkg", "linux-cachyos updated to CachyOS's current version"] },
|
||||||
|
hdmi: { label: "HDMI refresh boost", hint: "No longer needed: removed",
|
||||||
|
body: "Made HDMI displays run above 60 Hz on the pinned kernel. Current kernels do that themselves, so what's left of it is removed and every display uses its own EDID again.",
|
||||||
|
changes: ["Saved display EDIDs and the hotplug service removed", "The old kernel parameter removed, if it's still there"] },
|
||||||
|
bios: { label: "Update BIOS", hint: "",
|
||||||
|
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
|
||||||
|
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
|
||||||
|
})
|
||||||
|
// Replaces the explanation of an option that can't be turned on here.
|
||||||
|
readonly property var unsupported: ({
|
||||||
|
// The body comes from the backend (item.note): it depends on the driver.
|
||||||
|
vram: { changes: ["Nothing until then"] }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Per plan action: the review's chip [text, colour, background] and the
|
||||||
|
// progress screen's verb.
|
||||||
|
readonly property var actions: ({
|
||||||
|
on: ["Turn on", Theme.good, Theme.goodBg, "Turn on "], off: ["Turn off", Theme.bad, Theme.badBg, "Turn off "],
|
||||||
|
again: ["Re-apply", Theme.info, Theme.infoBg, "Re-apply "], update: ["Update", Theme.warn, Theme.warnBg, ""],
|
||||||
|
desktop: ["Desktop", Theme.info, Theme.infoBg, "Boot into "], gaming: ["Gaming", Theme.info, Theme.infoBg, "Boot into "],
|
||||||
|
check: ["Check", Theme.warn, Theme.warnBg, "Download and check the "], flash: ["Flash", Theme.bad, Theme.badBg, "Hand to fwupd: the "]
|
||||||
|
})
|
||||||
|
|
||||||
|
function of(item) {
|
||||||
|
var tx = items[item.id] || {};
|
||||||
|
if (item.selectable !== false || !unsupported[item.id]) return tx;
|
||||||
|
return Object.assign({}, tx, unsupported[item.id], item.note ? { body: item.note } : {});
|
||||||
|
}
|
||||||
|
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Colours and fonts (from the design).
|
||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
readonly property color bg: "#0f1319"
|
||||||
|
readonly property color bar: "#141a22"
|
||||||
|
readonly property color line: "#232c38"
|
||||||
|
readonly property color card: "#161d27"
|
||||||
|
readonly property color cardSel: "#1f2a3a"
|
||||||
|
readonly property color text: "#e6ebf2"
|
||||||
|
readonly property color textHi: "#eef2f7"
|
||||||
|
readonly property color soft: "#c3ccd8"
|
||||||
|
readonly property color mute: "#97a3b4"
|
||||||
|
readonly property color faint: "#8b97a8"
|
||||||
|
readonly property color label: "#b8c3d1"
|
||||||
|
readonly property color accent: "#3ea6ff"
|
||||||
|
readonly property color ink: "#06121f"
|
||||||
|
readonly property color good: "#7fd1a8"
|
||||||
|
readonly property color goodBg: "#173226"
|
||||||
|
readonly property color warn: "#f2a33a"
|
||||||
|
readonly property color warnBg: "#2a2116"
|
||||||
|
readonly property color bad: "#ff9a8a"
|
||||||
|
readonly property color badBg: "#2a1c1c"
|
||||||
|
readonly property color info: "#7cc4ff"
|
||||||
|
readonly property color infoBg: "#1b2b40"
|
||||||
|
readonly property color note: "#1a2230"
|
||||||
|
readonly property color key: "#2a3444"
|
||||||
|
readonly property color button: "#232c38"
|
||||||
|
readonly property color danger: "#8a2c2c"
|
||||||
|
readonly property color dangerBg: "#1d1416"
|
||||||
|
readonly property color dangerLine: "#b33a3a"
|
||||||
|
readonly property string display: "Barlow Semi Condensed"
|
||||||
|
readonly property string body: "IBM Plex Sans"
|
||||||
|
readonly property string mono: "IBM Plex Mono"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
singleton Theme 1.0 Theme.qml
|
||||||
|
singleton Texts 1.0 Texts.qml
|
||||||
|
singleton Input 1.0 Input.qml
|
||||||
|
AppState 1.0 AppState.qml
|
||||||
|
Glyph 1.0 Glyph.qml
|
||||||
|
Btn 1.0 Btn.qml
|
||||||
|
Chip 1.0 Chip.qml
|
||||||
|
Badge 1.0 Badge.qml
|
||||||
|
Bar 1.0 Bar.qml
|
||||||
|
RiskList 1.0 RiskList.qml
|
||||||
|
Header 1.0 Header.qml
|
||||||
|
MenuRow 1.0 MenuRow.qml
|
||||||
|
DetailPanel 1.0 DetailPanel.qml
|
||||||
|
MenuScreen 1.0 MenuScreen.qml
|
||||||
|
ReviewScreen 1.0 ReviewScreen.qml
|
||||||
|
PasswordScreen 1.0 PasswordScreen.qml
|
||||||
|
ApplyingScreen 1.0 ApplyingScreen.qml
|
||||||
|
BiosWarningScreen 1.0 BiosWarningScreen.qml
|
||||||
|
BiosConfirmScreen 1.0 BiosConfirmScreen.qml
|
||||||
|
DoneScreen 1.0 DoneScreen.qml
|
||||||
+10
-5
@@ -1,12 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Steamify CachyOS - get and start the app (v2), for `curl | bash`:
|
# Steamify CachyOS - get and start the app, for `curl | bash`:
|
||||||
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | 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,
|
# 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.
|
# it's missing, adds a launcher entry, and starts the app.
|
||||||
# STEAMIFY_RELEASE=<tag> takes that release instead of the newest (the
|
# STEAMIFY_RELEASE=<tag> takes that release instead of the newest.
|
||||||
# v2-ui-preview build's copy of this script sets it). STEAMIFY_BRANCH=<branch>
|
# STEAMIFY_BRANCH=<branch> takes that branch's source instead (for testing
|
||||||
# takes that branch's source instead (for testing before a build).
|
# before a build).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="theupriser/steamify-cachyos"
|
REPO="theupriser/steamify-cachyos"
|
||||||
@@ -71,4 +71,9 @@ EOF
|
|||||||
command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null || true
|
command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null || true
|
||||||
|
|
||||||
say "Starting Steamify..."
|
say "Starting Steamify..."
|
||||||
|
# Started from Steam: stay in the foreground, or Steam counts the game as
|
||||||
|
# ended right away and stops giving it the controller.
|
||||||
|
if [[ -n "${SteamGameId:-}" ]]; then
|
||||||
|
exec "$DEST/ui/steamify-ui" < /dev/null
|
||||||
|
fi
|
||||||
setsid "$DEST/ui/steamify-ui" >/dev/null 2>&1 < /dev/null &
|
setsid "$DEST/ui/steamify-ui" >/dev/null 2>&1 < /dev/null &
|
||||||
+13
-24
@@ -20,6 +20,14 @@ import tempfile
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Started from Steam (a non-Steam game, so Steam Input hands it the
|
||||||
|
# controller): Steam preloads its overlay into every game, and Qt aborts with
|
||||||
|
# it loaded. Start again without it, before Qt is imported.
|
||||||
|
if "gameoverlayrenderer" in os.environ.get("LD_PRELOAD", ""):
|
||||||
|
os.environ["LD_PRELOAD"] = ":".join(p for p in os.environ["LD_PRELOAD"].split(":")
|
||||||
|
if p and "gameoverlayrenderer" not in p)
|
||||||
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -271,8 +279,8 @@ class Backend(QObject):
|
|||||||
self.busyChanged.emit()
|
self.busyChanged.emit()
|
||||||
p.start()
|
p.start()
|
||||||
|
|
||||||
@Slot("QVariant", str, bool, str, str)
|
@Slot("QVariant", str, bool, str)
|
||||||
def apply(self, ids, boot, reapply, password, hdmi):
|
def apply(self, ids, boot, reapply, password):
|
||||||
try:
|
try:
|
||||||
if hasattr(ids, "toVariant"): # a JS array from QML
|
if hasattr(ids, "toVariant"): # a JS array from QML
|
||||||
ids = ids.toVariant()
|
ids = ids.toVariant()
|
||||||
@@ -281,8 +289,6 @@ class Backend(QObject):
|
|||||||
args.append("--reapply")
|
args.append("--reapply")
|
||||||
if boot:
|
if boot:
|
||||||
args += ["--boot", boot]
|
args += ["--boot", boot]
|
||||||
if hdmi: # rates confirmed on the HDMI screen
|
|
||||||
args += ["--hdmi", hdmi]
|
|
||||||
self._start(args + [str(i) for i in (ids or [])], password)
|
self._start(args + [str(i) for i in (ids or [])], password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Never leave the app on the progress screen.
|
# Never leave the app on the progress screen.
|
||||||
@@ -300,26 +306,6 @@ class Backend(QObject):
|
|||||||
"""Only after both warnings in the app."""
|
"""Only after both warnings in the app."""
|
||||||
self._start(["bios-flash"], password)
|
self._start(["bios-flash"], password)
|
||||||
|
|
||||||
@Slot(str)
|
|
||||||
def hdmiOptions(self, password):
|
|
||||||
"""Reads the HDMI displays: ends with an hdmi-options event."""
|
|
||||||
self._start(["hdmi-options"], password)
|
|
||||||
|
|
||||||
@Slot(str, str, int, int, float, str)
|
|
||||||
def hdmiTry(self, password, conn, w, h, hz, rates):
|
|
||||||
"""Switches to hz live: an hdmi-tried event says whether it worked."""
|
|
||||||
self._start(["hdmi-try", conn, str(w), str(h), "%g" % hz] + [r for r in rates.split(",") if r], password)
|
|
||||||
|
|
||||||
@Slot(str, str, int, int, float)
|
|
||||||
def hdmiReset(self, password, conn, w, h, hz):
|
|
||||||
"""The display's own EDID and the old rate again (hdmi-reset event)."""
|
|
||||||
self._start(["hdmi-reset", conn, str(w), str(h), "%g" % hz], password)
|
|
||||||
|
|
||||||
@Slot(str, str)
|
|
||||||
def hdmiForget(self, password, display):
|
|
||||||
"""Removes a saved display (hdmi-forgotten event)."""
|
|
||||||
self._start(["hdmi-forget", display], password)
|
|
||||||
|
|
||||||
def _read(self, p):
|
def _read(self, p):
|
||||||
self._buf += bytes(p.readAllStandardOutput())
|
self._buf += bytes(p.readAllStandardOutput())
|
||||||
while b"\n" in self._buf:
|
while b"\n" in self._buf:
|
||||||
@@ -387,6 +373,9 @@ def main():
|
|||||||
engine = QQmlApplicationEngine()
|
engine = QQmlApplicationEngine()
|
||||||
engine.setInitialProperties({"backend": backend, "gamepad": pad,
|
engine.setInitialProperties({"backend": backend, "gamepad": pad,
|
||||||
"fullscreen": fullscreen,
|
"fullscreen": fullscreen,
|
||||||
|
# Started from Steam: its keys are a Steam
|
||||||
|
# Controller's buttons (desktop layout).
|
||||||
|
"fromSteam": bool(os.environ.get("SteamGameId")),
|
||||||
"iconUrl": QUrl.fromLocalFile(os.path.realpath(icon)).toString()})
|
"iconUrl": QUrl.fromLocalFile(os.path.realpath(icon)).toString()})
|
||||||
engine.load(os.path.join(HERE, "qml", "Main.qml"))
|
engine.load(os.path.join(HERE, "qml", "Main.qml"))
|
||||||
if not engine.rootObjects():
|
if not engine.rootObjects():
|
||||||
|
|||||||
Reference in new issue
Block a user