From 3196812c0d50002f5e057940fa32a5bcbf803c7f Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 17:43:36 +0800 Subject: [PATCH 1/9] fix(plugin): dropdown data prop, always show guide, add config export/import --- plugins/decky-vault/main.py | 36 ++++- .../src/components/session-form.tsx | 22 +-- .../src/components/settings-panel.tsx | 152 ++++++++++++------ plugins/decky-vault/src/lib/api.ts | 25 ++- 4 files changed, 172 insertions(+), 63 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 2a2990b..0e562b2 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -413,4 +413,38 @@ benchmark_percentiles=97,AVG,1,0.1 except urllib.error.URLError as e: return {"valid": False, "error": f"Network error: {str(e.reason)}"} except Exception as e: - return {"valid": False, "error": str(e)} \ No newline at end of file + return {"valid": False, "error": str(e)} + + async def export_config(self, settings: dict) -> dict: + """RPC: Export current settings to Downloads/deckyvault-config.json. + Returns {success: bool, path?: str, error?: str}.""" + try: + home = os.path.expanduser("~") + config_path = os.path.join(home, "Downloads", "deckyvault-config.json") + with open(config_path, 'w') as f: + json.dump(settings, f, indent=2) + return {"success": True, "path": config_path} + except Exception as e: + return {"success": False, "error": str(e)} + + async def import_config(self) -> dict: + """RPC: Import settings from the latest deckyvault-config.json in Downloads. + Returns {success: bool, settings?: dict, error?: str}.""" + try: + home = os.path.expanduser("~") + config_path = os.path.join(home, "Downloads", "deckyvault-config.json") + if not os.path.exists(config_path): + return {"success": False, "error": "No deckyvault-config.json found in Downloads"} + with open(config_path, 'r') as f: + settings = json.load(f) + return { + "success": True, + "settings": { + "apiKey": settings.get("apiKey", ""), + "exportPath": settings.get("exportPath", "/home/deck/Downloads"), + "baseUrl": settings.get("baseUrl", "https://deckyvault.xyz"), + "hardwareSlug": settings.get("hardwareSlug", None), + } + } + except Exception as e: + return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/plugins/decky-vault/src/components/session-form.tsx b/plugins/decky-vault/src/components/session-form.tsx index 05c69de..5a7d39b 100644 --- a/plugins/decky-vault/src/components/session-form.tsx +++ b/plugins/decky-vault/src/components/session-form.tsx @@ -29,20 +29,20 @@ interface SessionFormProps { } const UPSCALER_OPTIONS = [ - { label: "None", value: "none" }, - { label: "FSR", value: "fsr" }, - { label: "DLSS", value: "dlss" }, - { label: "XeSS", value: "xess" }, - { label: "LSFG", value: "lsfg" }, - { label: "Other", value: "other" }, + { label: "None", data: "none" }, + { label: "FSR", data: "fsr" }, + { label: "DLSS", data: "dlss" }, + { label: "XeSS", data: "xess" }, + { label: "LSFG", data: "lsfg" }, + { label: "Other", data: "other" }, ] const FRAME_GEN_OPTIONS = [ - { label: "None", value: "none" }, - { label: "FSR FG", value: "fsr_fg" }, - { label: "DLSS FG", value: "dlss_fg" }, - { label: "LSFG", value: "lsfg" }, - { label: "Other", value: "other" }, + { label: "None", data: "none" }, + { label: "FSR FG", data: "fsr_fg" }, + { label: "DLSS FG", data: "dlss_fg" }, + { label: "LSFG", data: "lsfg" }, + { label: "Other", data: "other" }, ] export default function SessionForm({ diff --git a/plugins/decky-vault/src/components/settings-panel.tsx b/plugins/decky-vault/src/components/settings-panel.tsx index 0886fd9..08ee14b 100644 --- a/plugins/decky-vault/src/components/settings-panel.tsx +++ b/plugins/decky-vault/src/components/settings-panel.tsx @@ -12,10 +12,12 @@ import { FaTimes, FaDownload, FaCog, + FaFileExport, + FaFileImport, } from "react-icons/fa" import type { PluginSettings } from "../lib/store" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" -import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig } from "../lib/api" +import { testApiKey, checkMangohud, writeMangohudConfig, exportConfig, importConfig } from "../lib/api" interface SettingsPanelProps { settings: PluginSettings @@ -26,8 +28,8 @@ interface SettingsPanelProps { } const HARDWARE_OPTIONS = [ - { label: "Auto-detect", value: "" }, - ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, value: slug })), + { label: "Auto-detect", data: "" }, + ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })), ] export default function SettingsPanel({ @@ -42,8 +44,8 @@ export default function SettingsPanel({ path: string version: string }>({ checked: false, installed: false, path: "", version: "" }) - const [showMangohudGuide, setShowMangohudGuide] = useState(false) const [configWritten, setConfigWritten] = useState(false) + const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null) async function handleTestKey() { if (!settings.apiKey) { @@ -78,6 +80,30 @@ export default function SettingsPanel({ setConfigWritten(result.success) } + async function handleExportConfig() { + setConfigStatus(null) + const result = await exportConfig(settings) + if (result.success) { + setConfigStatus({ message: `Config saved to ${result.path}`, isError: false }) + } else { + setConfigStatus({ message: result.error || "Export failed", isError: true }) + } + } + + async function handleImportConfig() { + setConfigStatus(null) + const result = await importConfig() + if (result.success && result.settings) { + onUpdateSetting("apiKey", result.settings.apiKey || "") + onUpdateSetting("exportPath", result.settings.exportPath || "/home/deck/Downloads") + onUpdateSetting("baseUrl", result.settings.baseUrl || "https://deckyvault.xyz") + onUpdateSetting("hardwareSlug", result.settings.hardwareSlug || null) + setConfigStatus({ message: "Config imported from Downloads", isError: false }) + } else { + setConfigStatus({ message: result.error || "No config file found in Downloads", isError: true }) + } + } + return ( <> {/* ── API Key ─────────────────────────────────────────────── */} @@ -152,6 +178,40 @@ export default function SettingsPanel({ + {/* ── Config Export/Import ────────────────────────────────── */} + + + +
+ + Export Config to Downloads +
+
+
+ + +
+ + Import Config from Downloads +
+
+
+ {configStatus && ( + +
+ {configStatus.isError ? : } {configStatus.message} +
+
+ )} +
+ {/* ── MangoHud Setup ──────────────────────────────────────── */} @@ -205,53 +265,45 @@ export default function SettingsPanel({ )} - setShowMangohudGuide(!showMangohudGuide)}> - {showMangohudGuide ? "Hide Guide" : "Show Installation Guide"} - +
+ Steam Deck (SteamOS): +
+ MangoHud is pre-installed. Enable it per-game by adding + + mangohud %command% + + to the game's Steam launch options (right-click game → Properties → Launch Options). + +

+ Other Linux handhelds (ROG Ally, Legion Go): +
+ Install via package manager: + + sudo apt install mangohud + + or Flatpak: + + flatpak install flathub org.freedesktop.Platform.VulkanLayer.MangoHud + + +

+ Manual build: +
+ See{" "} + + github.com/flightlessmango/MangoHud + + +

+ Troubleshooting: +
+ • Log file empty? Check MangoHud is enabled for the game and the config was written. +
+ • Wrong path? Ensure the plugin can write to /tmp/. +
+ • Not attaching? Try adding mangohud %command% to Steam launch options explicitly. +
- - {showMangohudGuide && ( - -
- Steam Deck (SteamOS): -
- MangoHud is pre-installed. Enable it per-game by adding - - mangohud %command% - - to the game's Steam launch options (right-click game → Properties → Launch Options). - -

- Other Linux handhelds (ROG Ally, Legion Go): -
- Install via package manager: - - sudo apt install mangohud - - or Flatpak: - - flatpak install flathub org.freedesktop.Platform.VulkanLayer.MangoHud - - -

- Manual build: -
- See{" "} - - github.com/flightlessmango/MangoHud - - -

- Troubleshooting: -
- • Log file empty? Check MangoHud is enabled for the game and the config was written. -
- • Wrong path? Ensure the plugin can write to /tmp/. -
- • Not attaching? Try adding mangohud %command% to Steam launch options explicitly. -
-
- )}
) diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index c6db3e5..fe8dddd 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -73,4 +73,27 @@ export const uploadToDeckyvault = callable<[ export const testApiKey = callable<[apiKey: string, baseUrl?: string], { valid: boolean error?: string -}>("test_api_key") \ No newline at end of file +}>("test_api_key") + +// ── Config Export/Import ──────────────────────────────────────── +export const exportConfig = callable<[settings: { + apiKey: string + exportPath: string + baseUrl: string + hardwareSlug: string | null +}], { + success: boolean + path?: string + error?: string +}>("export_config") + +export const importConfig = callable<[], { + success: boolean + settings?: { + apiKey: string + exportPath: string + baseUrl: string + hardwareSlug: string | null + } + error?: string +}>("import_config") \ No newline at end of file From bd14fdaf8258b8761ee897790785d558fb53fe4a Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 17:45:32 +0800 Subject: [PATCH 2/9] feat(web): add Download Plugin Config button to API key creation --- .../profile/settings-api-keys-tab.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/web/components/profile/settings-api-keys-tab.tsx b/apps/web/components/profile/settings-api-keys-tab.tsx index 12df467..dadde7f 100644 --- a/apps/web/components/profile/settings-api-keys-tab.tsx +++ b/apps/web/components/profile/settings-api-keys-tab.tsx @@ -13,6 +13,7 @@ import { EyeOff, Clock, AlertCircle, + Download, } from "lucide-react" import { motion, AnimatePresence } from "motion/react" @@ -187,6 +188,27 @@ export function SettingsApiKeysTab() { } } + const handleDownloadConfig = () => { + if (!createdKey?.key) return + const config = { + apiKey: createdKey.key, + exportPath: "/home/deck/Downloads", + baseUrl: "https://deckyvault.xyz", + hardwareSlug: null, + } + const blob = new Blob([JSON.stringify(config, null, 2)], { + type: "application/json", + }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "deckyvault-config.json" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + // Show the created key modal @@ -261,6 +283,14 @@ export function SettingsApiKeysTab() { > Done + + ) From f7d36e82ef0d0bdd33ffc7c663329f6144c8f734 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 17:59:05 +0800 Subject: [PATCH 3/9] feat(plugin): add verify config and copy launch option buttons --- .../src/components/settings-panel.tsx | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/plugins/decky-vault/src/components/settings-panel.tsx b/plugins/decky-vault/src/components/settings-panel.tsx index 08ee14b..efad310 100644 --- a/plugins/decky-vault/src/components/settings-panel.tsx +++ b/plugins/decky-vault/src/components/settings-panel.tsx @@ -14,10 +14,12 @@ import { FaCog, FaFileExport, FaFileImport, + FaCopy, + FaSearch, } from "react-icons/fa" import type { PluginSettings } from "../lib/store" import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" -import { testApiKey, checkMangohud, writeMangohudConfig, exportConfig, importConfig } from "../lib/api" +import { testApiKey, checkMangohud, writeMangohudConfig, getMangohudConfig, exportConfig, importConfig } from "../lib/api" interface SettingsPanelProps { settings: PluginSettings @@ -46,6 +48,12 @@ export default function SettingsPanel({ }>({ checked: false, installed: false, path: "", version: "" }) const [configWritten, setConfigWritten] = useState(false) const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null) + const [configVerified, setConfigVerified] = useState<{ + checked: boolean + valid: boolean + message: string + }>({ checked: false, valid: false, message: "" }) + const [copiedLaunchOpt, setCopiedLaunchOpt] = useState(false) async function handleTestKey() { if (!settings.apiKey) { @@ -80,6 +88,62 @@ export default function SettingsPanel({ setConfigWritten(result.success) } + async function handleVerifyConfig() { + const result = await getMangohudConfig() + if (!result.exists) { + setConfigVerified({ + checked: true, + valid: false, + message: "No MangoHud config found. Write one first.", + }) + return + } + const content = result.content + const hasOutputFolder = content.includes("output_folder=/tmp") + const hasOutputFile = content.includes("output_file=deckyvault-mangohud.log") + const hasFps = content.includes("fps") + const hasFrameTiming = content.includes("frame_timing") + const hasGpuPower = content.includes("gpu_power") + + if (hasOutputFolder && hasOutputFile && hasFps) { + setConfigVerified({ + checked: true, + valid: true, + message: "Config looks good — logging to /tmp/deckyvault-mangohud.log", + }) + } else { + const missing: string[] = [] + if (!hasOutputFolder) missing.push("output_folder=/tmp") + if (!hasOutputFile) missing.push("output_file=deckyvault-mangohud.log") + if (!hasFps) missing.push("fps") + if (!hasFrameTiming) missing.push("frame_timing") + if (!hasGpuPower) missing.push("gpu_power") + setConfigVerified({ + checked: true, + valid: false, + message: `Missing: ${missing.join(", ")}. Write config again.`, + }) + } + } + + async function handleCopyLaunchOption() { + try { + await navigator.clipboard.writeText("mangohud %command%") + setCopiedLaunchOpt(true) + setTimeout(() => setCopiedLaunchOpt(false), 2000) + } catch { + // Fallback + const ta = document.createElement("textarea") + ta.value = "mangohud %command%" + document.body.appendChild(ta) + ta.select() + document.execCommand("copy") + document.body.removeChild(ta) + setCopiedLaunchOpt(true) + setTimeout(() => setCopiedLaunchOpt(false), 2000) + } + } + async function handleExportConfig() { setConfigStatus(null) const result = await exportConfig(settings) @@ -256,6 +320,39 @@ export default function SettingsPanel({ + + +
+ + Verify Config +
+
+
+ + {configVerified.checked && ( + +
+ {configVerified.valid ? : } {configVerified.message} +
+
+ )} + + + +
+ + {copiedLaunchOpt ? "Copied!" : "Copy Launch Option"} +
+
+
+ {configWritten && (
From f97bc0b8b7e88f3caf5c43f6de2b6caad47e4ae9 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:06:13 +0800 Subject: [PATCH 4/9] feat(plugin): use DeckyVault brand SVG icon --- plugins/decky-vault/src/index.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 9dc1561..19587ff 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -7,7 +7,6 @@ import { import { definePlugin, } from "@decky/api" -import { FaDatabase } from "react-icons/fa" import MainPanel from "./components/main-panel" import SettingsPanel from "./components/settings-panel" import { useSettings, useSession } from "./lib/store" @@ -163,12 +162,36 @@ function Content() { ) } +// ── DeckyVault icon (flat SVG) ──────────────────────────────── +function DeckyVaultIcon() { + return ( + + + + + + + ) +} + export default definePlugin(() => { return { name: "DeckyVault", titleView:
DeckyVault
, content: , - icon: , + icon: , alwaysRender: false, onDismount() { console.log("[DeckyVault] Plugin unloading") From 7b2f031c3be3a49a567f0074f3c55a1f5546efc4 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:28:28 +0800 Subject: [PATCH 5/9] fix(plugin): fix SVG viewBox for proper icon scaling --- plugins/decky-vault/src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 19587ff..77390b1 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -166,13 +166,13 @@ function Content() { function DeckyVaultIcon() { return ( - + Date: Sun, 28 Jun 2026 18:31:53 +0800 Subject: [PATCH 6/9] fix(plugin): use full DeckyVault SVG icon with all paths and transforms --- plugins/decky-vault/src/index.tsx | 34 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 77390b1..575409b 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -166,21 +166,33 @@ function Content() { function DeckyVaultIcon() { return ( - - - + + + + + + + + + + + + ) From 1ea079917f3d3e54166575e68d328bb4538a6ac9 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:36:24 +0800 Subject: [PATCH 7/9] fix(plugin): add second gear path layer for proper icon rendering --- plugins/decky-vault/src/index.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 575409b..aa682af 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -170,13 +170,19 @@ function DeckyVaultIcon() { width="24" height="24" xmlns="http://www.w3.org/2000/svg" - style={{ fillRule: "evenodd", clipRule: "evenodd", strokeLinecap: "round", strokeLinejoin: "round" }} + style={{ + fillRule: "evenodd", + clipRule: "evenodd", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeMiterlimit: 1.5, + }} > @@ -192,6 +198,10 @@ function DeckyVaultIcon() { d="M323,1000L323,891.398C323,841.926 363.195,801.76 412.704,801.76L640.543,801.76L479.436,640.773C444.428,605.791 444.428,548.988 479.436,514.006L563.141,430.363C598.149,395.381 654.994,395.381 690.002,430.363L851.108,591.351L851.108,363.68C851.108,314.208 891.303,274.042 940.812,274.042L1059.188,274.042C1108.697,274.042 1148.892,314.208 1148.892,363.68L1148.892,591.351L1309.998,430.363C1345.006,395.381 1401.851,395.381 1436.859,430.363L1520.564,514.006C1555.572,548.988 1555.572,605.791 1520.564,640.773L1359.457,801.76L1587.296,801.76C1636.805,801.76 1677,841.926 1677,891.398L1677,1000L1180.034,1000C1184.251,984.222 1186.5,967.643 1186.5,950.542C1186.5,844.971 1100.789,759.26 995.218,759.26C889.646,759.26 803.935,844.971 803.935,950.542C803.935,967.643 806.184,984.222 810.401,1000L323,1000Z" fill="white" stroke="white" strokeWidth="50" /> + From 297fbc03207cffdb7373b17c1340fd71b7d6e70d Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:40:09 +0800 Subject: [PATCH 8/9] fix(plugin): remove explicit icon size, let container handle scaling --- plugins/decky-vault/src/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index aa682af..f9d1a54 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -167,8 +167,6 @@ function DeckyVaultIcon() { return ( Date: Sun, 28 Jun 2026 18:43:56 +0800 Subject: [PATCH 9/9] fix(plugin): set icon to 16x16 --- plugins/decky-vault/src/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index f9d1a54..8c0a411 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -7,6 +7,7 @@ import { import { definePlugin, } from "@decky/api" +import { FaChartLine } from "react-icons/fa" import MainPanel from "./components/main-panel" import SettingsPanel from "./components/settings-panel" import { useSettings, useSession } from "./lib/store" @@ -167,6 +168,8 @@ function DeckyVaultIcon() { return (