From 22bc81238d308dad50c582f04a8695e9399c3abe Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 5 Aug 2026 23:20:28 +0200 Subject: [PATCH] fix(decky): Decky's plugin list says "Punktfunk", not "punktfunk" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The label Decky shows for an installed plugin is plugin.json "name", which we had set to the lowercase directory name — so the one place every user sees the plugin listed was the one place it was off-brand, while the panel header (titleView) already read "Punktfunk". The two were conflated because the name looked load-bearing: the zip's top-level dir becomes ~/homebrew/plugins/, and the scripts derived that dir FROM plugin.json "name". They are in fact independent — Decky extracts the zip as-is and locates an installed plugin by MATCHING plugin.json "name", never by folder name (that is how a plugin can live in DeckWebBrowser/ and list itself as "Web Browser"). So brand-case the label and pin the on-disk dir to the literal `punktfunk` in package.sh/deploy.sh/CI instead of deriving it. Pinning is the part that matters: had the dir followed the label, this rename would have installed a second `Punktfunk/` folder beside the existing `punktfunk/` and the plugin would have shown up twice. The self-update call passes the name Decky uninstalls before extracting, so it moves to "Punktfunk" with it. The upgrade INTO this build still passes "punktfunk" (the installed build's own value), which matches that build's plugin.json — so the old folder is removed and the new zip lands in the same lowercase dir either way. Decky's per-plugin settings dir is unused (all state lives in ~/.config/punktfunk), so nothing is stranded. --- .gitea/workflows/decky.yml | 5 ++++- clients/decky/plugin.json | 2 +- clients/decky/scripts/deploy.sh | 4 +++- clients/decky/scripts/package.sh | 12 ++++++++---- clients/decky/src/hooks.ts | 5 ++++- clients/decky/src/index.tsx | 8 +++++--- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/decky.yml b/.gitea/workflows/decky.yml index a5363174..e72bde05 100644 --- a/.gitea/workflows/decky.yml +++ b/.gitea/workflows/decky.yml @@ -46,7 +46,10 @@ env: REGISTRY: git.unom.io OWNER: unom PACKAGE: punktfunk-decky # generic-registry package name - PLUGIN: punktfunk # plugin.json "name" == zip top-level dir + # The plugin's ON-DISK dir == the zip's top-level dir. Deliberately NOT plugin.json "name" + # (that is the brand-cased label Decky lists, and it locates a plugin by matching it, not by + # the folder) — see clients/decky/scripts/package.sh. + PLUGIN: punktfunk jobs: build-publish: diff --git a/clients/decky/plugin.json b/clients/decky/plugin.json index 9f179723..dc6a5f77 100644 --- a/clients/decky/plugin.json +++ b/clients/decky/plugin.json @@ -1,5 +1,5 @@ { - "name": "punktfunk", + "name": "Punktfunk", "author": "enrico", "flags": ["debug"], "api_version": 1, diff --git a/clients/decky/scripts/deploy.sh b/clients/decky/scripts/deploy.sh index 2d3eb202..db004de3 100755 --- a/clients/decky/scripts/deploy.sh +++ b/clients/decky/scripts/deploy.sh @@ -12,7 +12,9 @@ set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" DECK="${DECK:?set DECK=deck@}" -NAME="$(python3 -c 'import json;print(json.load(open("'"$HERE"'/plugin.json"))["name"])')" +# The on-disk plugin DIR (what scripts/package.sh staged into out/), not plugin.json "name" — +# that field is the brand-cased label Decky shows in its plugin list. See package.sh's header. +NAME=punktfunk STAGE_LOCAL="$HERE/out/$NAME" [ -d "$STAGE_LOCAL" ] || { echo "$STAGE_LOCAL missing — run scripts/package.sh first" >&2; exit 1; } diff --git a/clients/decky/scripts/package.sh b/clients/decky/scripts/package.sh index 9c07cf12..6e517563 100755 --- a/clients/decky/scripts/package.sh +++ b/clients/decky/scripts/package.sh @@ -5,9 +5,13 @@ # package.json,decky.pyi,LICENSE,README.md} # out/punktfunk/ (the same tree, unzipped — rsync this with scripts/deploy.sh) # -# Decky extracts the zip with --strip-components=1, so the single top-level dir MUST equal -# plugin.json "name". Run after `pnpm build` (or use `pnpm run package`). Host-agnostic: needs -# only bash, python3 and zip. +# The single top-level dir is the plugin's ON-DISK folder name (Decky extracts the zip as-is, +# so the dir in the zip becomes ~/homebrew/plugins/). It is deliberately NOT read from +# plugin.json "name": that field is the user-visible label ("Punktfunk", brand-cased, shown in +# Decky's plugin list) and Decky locates an installed plugin by MATCHING it, never by the folder +# name. Keeping the folder lowercase means a rename of the label can't strand the old directory +# next to a new one (which would show up as two plugins). +# Run after `pnpm build` (or use `pnpm run package`). Host-agnostic: needs only bash, python3 and zip. set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" cd "$HERE" @@ -15,7 +19,7 @@ cd "$HERE" [ -f dist/index.js ] || { echo "dist/index.js missing — run 'pnpm build' first" >&2; exit 1; } [ -f LICENSE ] || { echo "LICENSE missing (required by the Decky store)" >&2; exit 1; } -NAME="$(python3 -c 'import json;print(json.load(open("plugin.json"))["name"])')" +NAME=punktfunk # the on-disk plugin dir (see the header) — NOT plugin.json "name" VER="$(python3 -c 'import json;print(json.load(open("package.json"))["version"])')" STAGE="$(mktemp -d)" diff --git a/clients/decky/src/hooks.ts b/clients/decky/src/hooks.ts index 0c1911dc..8f2fde12 100644 --- a/clients/decky/src/hooks.ts +++ b/clients/decky/src/hooks.ts @@ -387,7 +387,10 @@ export async function applyUpdate( // before any result could arrive — so never await it. Decky shows its own confirm prompt. void backend.callable("utilities/install_plugin")( info.artifact, - "punktfunk", + // The name Decky uninstalls before extracting the new zip — it locates the folder by + // matching plugin.json "name", so this must equal THIS build's plugin.json name (the + // brand-cased one), not the lowercase on-disk dir. + "Punktfunk", info.latest, info.hash, INSTALL_TYPE_UPDATE, diff --git a/clients/decky/src/index.tsx b/clients/decky/src/index.tsx index 24c5e180..e5ec1d6b 100644 --- a/clients/decky/src/index.tsx +++ b/clients/decky/src/index.tsx @@ -337,9 +337,11 @@ export default definePlugin(() => { // controller config. Fire-and-forget: cosmetic library upkeep must never block plugin load. void ensureGamepadUiShortcut(); return { - // `name` is the plugin's INTERNAL id — it must stay in sync with plugin.json (the loader - // keys plugins by it), so it stays lowercase; user-facing strings say "Punktfunk". - name: "punktfunk", + // `name` must stay in sync with plugin.json (the loader keys plugins by it) — and it is + // USER-VISIBLE: Decky labels the entry in its plugin list with it, so it carries the brand + // case. Decky finds an installed plugin by matching plugin.json "name" (never the folder + // name), so this is independent of the on-disk dir, which stays lowercase `punktfunk`. + name: "Punktfunk", // `staticClasses?.Title` is guarded so a future client that drops the export can't throw // at plugin-load time (an error boundary only catches render-time, not load-time, errors). titleView:
Punktfunk
,