From 80caa167ccab88a9d1b87ea3fa573727154eb01c Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:51:53 +0800 Subject: [PATCH 01/30] fix(plugin): use correct SteamClient API, alwaysRender true, wrap stop in try-catch --- plugins/decky-vault/src/index.tsx | 109 +++++++++++++++-------------- plugins/decky-vault/src/types.d.ts | 12 ++-- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 8c0a411..472f877 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -42,21 +42,15 @@ function Content() { // ── Register SteamClient game events ────────────────────────── useEffect(() => { try { - const startedReg = SteamClient.Apps.RegisterForGameStarted(async (appId: number) => { - let gameName = `App ${appId}` - try { - const info = await SteamClient.Apps.GetCurrentGameInfo() - if (info.appId === appId) { - gameName = info.strAppName - } - } catch { - // GetCurrentGameInfo may not be available in all contexts - } - onGameStart(appId, gameName) - }) + const startedReg = SteamClient.Apps.RegisterForGameActionStart( + (_gameActionId: number, appId: string, _action: string, _source: number) => { + const appIdNum = parseInt(appId, 10) + onGameStart(appIdNum, `App ${appId}`) + }, + ) gameStartedUnregRef.current = startedReg - const stoppedReg = SteamClient.Apps.RegisterForGameStopped((_appId: number) => { + const stoppedReg = SteamClient.Apps.RegisterForGameActionEnd((_gameActionId: number) => { onGameStop() }) gameStoppedUnregRef.current = stoppedReg @@ -83,49 +77,58 @@ function Content() { // ── Handle stop recording: parse log + read system info ──────── async function handleStop() { - stopRecording() + try { + stopRecording() - // Parse the MangoHud log - const logResult = await readAndParseMangohudLog() - if (logResult.error) { - setError(logResult.error) - // Still transition to stopped state so user can see the error + manual fields - return - } + // Parse the MangoHud log + const logResult = await readAndParseMangohudLog() + if (logResult.error) { + setError(logResult.error) + return + } - // Read system info in parallel - const [hwInfo, osVersion] = await Promise.all([ - getHardwareInfo(), - getOsVersion(), - ]) - - // Read Proton version + launch options if we have an app ID - let protonVersion = "" - let launchOptions = "" - if (session.appId) { - const [pv, lo] = await Promise.all([ - getProtonVersion(session.appId), - getLaunchOptions(session.appId), + // Read system info in parallel + const [hwInfo, osVersion] = await Promise.all([ + getHardwareInfo(), + getOsVersion(), ]) - protonVersion = pv - launchOptions = lo + + // Read Proton version + launch options if we have an app ID + let protonVersion = "" + let launchOptions = "" + const currentAppId = session.appId + if (currentAppId) { + try { + const [pv, lo] = await Promise.all([ + getProtonVersion(currentAppId), + getLaunchOptions(currentAppId), + ]) + protonVersion = pv + launchOptions = lo + } catch { + // Non-critical, continue without + } + } + + // Use settings hardware override if set, otherwise auto-detected + const hardwareSlug = settings.hardwareSlug || hwInfo.slug + + updateSession({ + fpsAvg: logResult.fpsAvg ?? null, + fpsLow: logResult.fpsLow ?? null, + fpsHigh: logResult.fpsHigh ?? null, + fpsOnePercentLow: logResult.fpsOnePercentLow ?? null, + tdpWatts: logResult.tdpWatts ?? null, + hardwareSlug, + hardwareName: hwInfo.name, + osVersion, + protonVersion, + launchOptions, + }) + } catch (e) { + console.error("[DeckyVault] Error stopping recording:", e) + setError("Failed to process recording. Check the MangoHud log.") } - - // Use settings hardware override if set, otherwise auto-detected - const hardwareSlug = settings.hardwareSlug || hwInfo.slug - - updateSession({ - fpsAvg: logResult.fpsAvg ?? null, - fpsLow: logResult.fpsLow ?? null, - fpsHigh: logResult.fpsHigh ?? null, - fpsOnePercentLow: logResult.fpsOnePercentLow ?? null, - tdpWatts: logResult.tdpWatts ?? null, - hardwareSlug, - hardwareName: hwInfo.name, - osVersion, - protonVersion, - launchOptions, - }) } if (!loaded) { @@ -215,7 +218,7 @@ export default definePlugin(() => { titleView:
DeckyVault
, content: , icon: , - alwaysRender: false, + alwaysRender: true, onDismount() { console.log("[DeckyVault] Plugin unloading") }, diff --git a/plugins/decky-vault/src/types.d.ts b/plugins/decky-vault/src/types.d.ts index a7fbb31..be14309 100644 --- a/plugins/decky-vault/src/types.d.ts +++ b/plugins/decky-vault/src/types.d.ts @@ -5,16 +5,12 @@ declare global { const SteamClient: { Apps: { - RegisterForGameStarted: ( - callback: (appId: number) => void, + RegisterForGameActionStart: ( + callback: (gameActionId: number, appId: string, action: string, source: number) => void, ) => { unregister: () => void } - RegisterForGameStopped: ( - callback: (appId: number) => void, + RegisterForGameActionEnd: ( + callback: (gameActionId: number) => void, ) => { unregister: () => void } - GetCurrentGameInfo: () => Promise<{ - appId: number - strAppName: string - }> } System: { GetOSVersion: () => Promise From a49aca93c7a42a823be87c013c49317964c8afa1 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:54:05 +0800 Subject: [PATCH 02/30] fix(plugin): add missing Field import for textareas --- plugins/decky-vault/src/components/session-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/decky-vault/src/components/session-form.tsx b/plugins/decky-vault/src/components/session-form.tsx index 5a7d39b..092cf8c 100644 --- a/plugins/decky-vault/src/components/session-form.tsx +++ b/plugins/decky-vault/src/components/session-form.tsx @@ -5,6 +5,7 @@ import { PanelSectionRow, DropdownItem, TextField, + Field, staticClasses, } from "@decky/ui" import { From 677adc5ad0b00bebf30740eaa2ccfbedbc4adcd4 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:54:46 +0800 Subject: [PATCH 03/30] fix(plugin): revert to RegisterForGameStarted/Stopped for runtime compat --- plugins/decky-vault/src/index.tsx | 13 ++++++------- plugins/decky-vault/src/types.d.ts | 6 ++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 472f877..c63bbe3 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -42,15 +42,14 @@ function Content() { // ── Register SteamClient game events ────────────────────────── useEffect(() => { try { - const startedReg = SteamClient.Apps.RegisterForGameActionStart( - (_gameActionId: number, appId: string, _action: string, _source: number) => { - const appIdNum = parseInt(appId, 10) - onGameStart(appIdNum, `App ${appId}`) - }, - ) + // Use RegisterForGameStarted/Stopped — these are the most widely used + // APIs in Decky plugins despite TypeScript type warnings. + const startedReg = SteamClient.Apps.RegisterForGameStarted((appId: number) => { + onGameStart(appId, `App ${appId}`) + }) gameStartedUnregRef.current = startedReg - const stoppedReg = SteamClient.Apps.RegisterForGameActionEnd((_gameActionId: number) => { + const stoppedReg = SteamClient.Apps.RegisterForGameStopped((_appId: number) => { onGameStop() }) gameStoppedUnregRef.current = stoppedReg diff --git a/plugins/decky-vault/src/types.d.ts b/plugins/decky-vault/src/types.d.ts index be14309..31cdccb 100644 --- a/plugins/decky-vault/src/types.d.ts +++ b/plugins/decky-vault/src/types.d.ts @@ -5,6 +5,12 @@ declare global { const SteamClient: { Apps: { + RegisterForGameStarted: ( + callback: (appId: number) => void, + ) => { unregister: () => void } + RegisterForGameStopped: ( + callback: (appId: number) => void, + ) => { unregister: () => void } RegisterForGameActionStart: ( callback: (gameActionId: number, appId: string, action: string, source: number) => void, ) => { unregister: () => void } From 7ab7af468a800d38c0a466162386cd6b24388d65 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:56:43 +0800 Subject: [PATCH 04/30] fix(plugin): autostart MangoHud logging on record, write config on start --- plugins/decky-vault/main.py | 2 +- plugins/decky-vault/src/index.tsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 0e562b2..49f5aec 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -192,7 +192,7 @@ class Plugin: # DeckyVault MangoHud logging config output_folder=/tmp output_file=deckyvault-mangohud.log -log_duration=0 +autostart_log=0 fps frame_timing cpu_power diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index c63bbe3..92732b4 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -14,6 +14,7 @@ import { useSettings, useSession } from "./lib/store" import { readAndParseMangohudLog, clearMangohudLog, + writeMangohudConfig, getHardwareInfo, getOsVersion, getProtonVersion, @@ -69,6 +70,8 @@ function Content() { // ── Handle start recording ──────────────────────────────────── async function handleStart() { + // Write MangoHud config with autostart_log so logging begins immediately + await writeMangohudConfig() // Clear any previous log file await clearMangohudLog() startRecording() From 65e56702b096a1557ac79aae71b11ae317cbe0cb Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:57:51 +0800 Subject: [PATCH 05/30] fix(plugin): add manual game name input, autostart MangoHud logging --- .../decky-vault/src/components/main-panel.tsx | 27 ++++++++++++++----- plugins/decky-vault/src/index.tsx | 2 ++ plugins/decky-vault/src/lib/store.ts | 8 ++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 3636445..dff0075 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -1,14 +1,15 @@ -import { useEffect, useState } from "react" import { ButtonItem, PanelSection, PanelSectionRow, + TextField, staticClasses, } from "@decky/ui" import { FaPlay, FaStop, FaClock, + FaGamepad, } from "react-icons/fa" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" import SessionForm from "./session-form" @@ -25,6 +26,7 @@ interface MainPanelProps { onAddToRecent: (sess: SessionData) => void onReset: () => void setError: (msg: string) => void + setGameName: (name: string, appId?: number) => void } export default function MainPanel({ @@ -39,6 +41,7 @@ export default function MainPanel({ onAddToRecent, onReset, setError, + setGameName, }: MainPanelProps) { const [elapsed, setElapsed] = useState(0) @@ -123,12 +126,22 @@ export default function MainPanel({ )} {recordingState === "idle" && ( - -
- Enable MangoHud for your game, then press Start Recording before launching. - Configure MangoHud in the Settings tab. -
-
+ <> + + setGameName(e.target.value)} + placeholder="e.g. Cyberpunk 2077" + /> + + +
+ Enable MangoHud for your game, then press Start Recording before launching. + Configure MangoHud in the Settings tab. +
+
+ )} {recentSessions.length > 0 && recordingState === "idle" && ( diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 92732b4..69a071e 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -36,6 +36,7 @@ function Content() { reset, onGameStart, onGameStop, + setGameName, } = useSession() const gameStartedUnregRef = useRef<{ unregister: () => void } | null>(null) const gameStoppedUnregRef = useRef<{ unregister: () => void } | null>(null) @@ -159,6 +160,7 @@ function Content() { onAddToRecent={addToRecent} onReset={reset} setError={setError} + setGameName={setGameName} /> { + currentAppNameRef.current = name + if (appId !== undefined) currentAppIdRef.current = appId + setSession((prev) => ({ ...prev, gameName: name, appId: appId ?? prev.appId })) + }, []) + return { recordingState, session, @@ -184,6 +191,7 @@ export function useSession() { reset, onGameStart, onGameStop, + setGameName, } } From 545d1c5cfa69146cadaebddad2bc2f5967d5cf2a Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 18:59:20 +0800 Subject: [PATCH 06/30] fix(plugin): add missing useState import --- plugins/decky-vault/src/components/main-panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index dff0075..908ac01 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react" import { ButtonItem, PanelSection, From c230367d141a202122a94f4aae928c9d2bd16416 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 19:04:35 +0800 Subject: [PATCH 07/30] fix(plugin): search for any MangoHud log in /tmp/ instead of fixed filename --- plugins/decky-vault/main.py | 48 ++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 49f5aec..f52813b 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -217,11 +217,42 @@ benchmark_percentiles=97,AVG,1,0.1 return {"exists": True, "content": f.read(), "path": config_path} return {"exists": False, "content": "", "path": config_path} - async def read_and_parse_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: + async def _find_mangohud_log(self) -> str | None: + """Find the most recent MangoHud log file in /tmp/. + MangoHud creates log files with the game name and timestamp.""" + import glob + # Look for any CSV or log files in /tmp that might be MangoHud logs + candidates = [] + for pattern in ["/tmp/*.csv", "/tmp/*.log", "/tmp/MangoHud*"]: + for f in glob.glob(pattern): + # Skip our own known file + if "deckyvault" in f: + candidates.append(f) + continue + # Check if the file starts with a MangoHud header + try: + with open(f, 'r') as fh: + first_line = fh.readline() + if 'MangoHud' in first_line or 'fps' in first_line.lower(): + candidates.append(f) + except (IOError, UnicodeDecodeError): + pass + if not candidates: + return None + # Return the most recently modified file + candidates.sort(key=lambda f: os.path.getmtime(f), reverse=True) + return candidates[0] + + async def read_and_parse_mangohud_log(self, log_path: str | None = None) -> dict: """RPC: Read the MangoHud log file and return parsed FPS stats. + If no log_path given, searches /tmp/ for the most recent MangoHud log. Returns parsed stats dict or {error: str}.""" + if log_path is None: + log_path = await self._find_mangohud_log() + if log_path is None: + return {"error": "No MangoHud log found in /tmp/. Make sure MangoHud is enabled and logging."} if not os.path.exists(log_path): - return {"error": f"MangoHud log not found at {log_path}. Make sure MangoHud is enabled and logging."} + return {"error": f"MangoHud log not found at {log_path}."} try: with open(log_path, 'r') as f: content = f.read() @@ -231,11 +262,16 @@ benchmark_percentiles=97,AVG,1,0.1 except Exception as e: return {"error": f"Failed to read log: {str(e)}"} - async def clear_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: - """RPC: Delete the MangoHud log file so the next recording starts fresh.""" + async def clear_mangohud_log(self) -> dict: + """RPC: Delete all MangoHud log files in /tmp/ so the next recording starts fresh.""" + import glob try: - if os.path.exists(log_path): - os.remove(log_path) + for pattern in ["/tmp/*.csv", "/tmp/*.log", "/tmp/MangoHud*"]: + for f in glob.glob(pattern): + try: + os.remove(f) + except (IOError, PermissionError): + pass return {"success": True} except Exception as e: return {"success": False, "error": str(e)} From 6e1957524b9e404693debab05b6cb1320bfaec51 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:21:33 +0800 Subject: [PATCH 08/30] feat(plugin): restructure layout like LSFG-VK, use mangohudctl for logging --- plugins/decky-vault/main.py | 35 +- .../decky-vault/src/components/main-panel.tsx | 463 +++++++++++++++--- plugins/decky-vault/src/index.tsx | 12 +- plugins/decky-vault/src/lib/api.ts | 12 +- 4 files changed, 441 insertions(+), 81 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index f52813b..f83b223 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -192,7 +192,6 @@ class Plugin: # DeckyVault MangoHud logging config output_folder=/tmp output_file=deckyvault-mangohud.log -autostart_log=0 fps frame_timing cpu_power @@ -217,6 +216,40 @@ benchmark_percentiles=97,AVG,1,0.1 return {"exists": True, "content": f.read(), "path": config_path} return {"exists": False, "content": "", "path": config_path} + async def start_mangohud_logging(self) -> dict: + """RPC: Start MangoHud logging via mangohudctl.""" + import subprocess + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "true"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + return {"success": True} + else: + return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} + except FileNotFoundError: + return {"success": False, "error": "mangohudctl not found. Is MangoHud running?"} + except Exception as e: + return {"success": False, "error": str(e)} + + async def stop_mangohud_logging(self) -> dict: + """RPC: Stop MangoHud logging via mangohudctl.""" + import subprocess + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "false"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + return {"success": True} + else: + return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} + except FileNotFoundError: + return {"success": False, "error": "mangohudctl not found. Is MangoHud running?"} + except Exception as e: + return {"success": False, "error": str(e)} + async def _find_mangohud_log(self) -> str | None: """Find the most recent MangoHud log file in /tmp/. MangoHud creates log files with the game name and timestamp.""" diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 908ac01..d80fcb1 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -4,15 +4,32 @@ import { PanelSection, PanelSectionRow, TextField, + DropdownItem, staticClasses, } from "@decky/ui" import { FaPlay, FaStop, FaClock, - FaGamepad, + FaCopy, + FaCheck, + FaTimes, + FaDownload, + FaCog, + FaFileExport, + FaFileImport, + FaSearch, } from "react-icons/fa" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" +import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" +import { + testApiKey, + checkMangohud, + writeMangohudConfig, + getMangohudConfig, + exportConfig, + importConfig, +} from "../lib/api" import SessionForm from "./session-form" interface MainPanelProps { @@ -28,8 +45,14 @@ interface MainPanelProps { onReset: () => void setError: (msg: string) => void setGameName: (name: string, appId?: number) => void + onUpdateSetting: (key: K, value: string | null) => void } +const HARDWARE_OPTIONS = [ + { label: "Auto-detect", data: "" }, + ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })), +] + export default function MainPanel({ recordingState, session, @@ -43,8 +66,25 @@ export default function MainPanel({ onReset, setError, setGameName, + onUpdateSetting, }: MainPanelProps) { const [elapsed, setElapsed] = useState(0) + const [mangohudStatus, setMangohudStatus] = useState<{ + checked: boolean + installed: boolean + path: string + version: string + }>({ checked: false, installed: false, path: "", version: "" }) + const [keyTestStatus, setKeyTestStatus] = useState<"idle" | "testing" | "valid" | "invalid">("idle") + const [keyTestMessage, setKeyTestMessage] = useState("") + const [copiedLaunchOpt, setCopiedLaunchOpt] = useState(false) + const [configWritten, setConfigWritten] = useState(false) + const [configVerified, setConfigVerified] = useState<{ + checked: boolean + valid: boolean + message: string + }>({ checked: false, valid: false, message: "" }) + const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null) // Timer for recording state useEffect(() => { @@ -64,6 +104,97 @@ export default function MainPanel({ return `${m}:${s.toString().padStart(2, "0")}` } + async function handleCheckMangohud() { + const result = await checkMangohud() + setMangohudStatus({ + checked: true, + installed: result.installed, + path: result.path, + version: result.version, + }) + } + + async function handleTestKey() { + if (!settings.apiKey) { + setKeyTestStatus("invalid") + setKeyTestMessage("Enter an API key first") + return + } + setKeyTestStatus("testing") + setKeyTestMessage("") + const result = await testApiKey(settings.apiKey, settings.baseUrl) + if (result.valid) { + setKeyTestStatus("valid") + setKeyTestMessage("API key is valid") + } else { + setKeyTestStatus("invalid") + setKeyTestMessage(result.error || "Invalid API key") + } + } + + async function handleCopyLaunchOption() { + try { + await navigator.clipboard.writeText("mangohud %command%") + setCopiedLaunchOpt(true) + setTimeout(() => setCopiedLaunchOpt(false), 2000) + } catch { + 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 handleWriteConfig() { + const result = await writeMangohudConfig() + 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") + if (hasOutputFolder && hasOutputFile && hasFps) { + setConfigVerified({ checked: true, valid: true, message: "Config looks good" }) + } else { + setConfigVerified({ checked: true, valid: false, message: "Config is missing required settings. Write it again." }) + } + } + + 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 }) + } + } + // ── Stopped state: show the session form ────────────────────── if (recordingState === "stopped") { return ( @@ -79,88 +210,272 @@ export default function MainPanel({ ) } - // ── Idle or Recording state ─────────────────────────────────── return ( - - {error && ( + <> + {/* ── Status ──────────────────────────────────────────────── */} + -
- {error} + +
+ + Check MangoHud Status +
+
+ + {mangohudStatus.checked && ( + +
+ {mangohudStatus.installed ? ( + <> MangoHud {mangohudStatus.version} + ) : ( + <> MangoHud not found + )} +
+
+ )} + + + {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} + {keyTestStatus === "valid" && } + {keyTestStatus === "invalid" && } + + + {keyTestMessage && ( + +
+ {keyTestMessage} +
+
+ )} + + + {/* ── Usage Instructions ──────────────────────────────────── */} + + +
+ Add this to your game's Steam launch options, then press Start Recording before launching.
- )} + +
+ mangohud %command% +
+
+ + +
+ + {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"} +
+
+
+ +
+ The MangoHud config is stored in ~/.config/MangoHud/MangoHud.conf +
+
+
- - {recordingState === "idle" ? ( - -
- - Start Recording -
-
- ) : ( - -
- - Stop Recording -
-
+ {/* ── Recording ──────────────────────────────────────────── */} + + {error && ( + +
{error}
+
)} -
- {recordingState === "recording" && ( - <> - -
-
- - {formatTime(elapsed)} -
-
- {session.gameName - ? `Recording: ${session.gameName}` - : "No game detected — recording anyway"} -
-
-
- - )} + {recordingState === "idle" && ( + <> + + setGameName(e.target.value)} + placeholder="e.g. Cyberpunk 2077" + /> + + + +
+ + Start Recording +
+
+
+ + )} - {recordingState === "idle" && ( - <> - - setGameName(e.target.value)} - placeholder="e.g. Cyberpunk 2077" - /> - - -
- Enable MangoHud for your game, then press Start Recording before launching. - Configure MangoHud in the Settings tab. -
-
- - )} - - {recentSessions.length > 0 && recordingState === "idle" && ( - - {recentSessions.map((rs, i) => ( - -
- {rs.gameName || "Unknown game"} -
- - {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} ·{" "} - {new Date(rs.date).toLocaleDateString()} - + {recordingState === "recording" && ( + <> + +
+
+ + {formatTime(elapsed)} +
+
+ {session.gameName ? `Recording: ${session.gameName}` : "Recording..."} +
- ))} - - )} - + + +
+ + Stop Recording +
+
+
+ + )} + + {recentSessions.length > 0 && recordingState === "idle" && ( + + {recentSessions.map((rs, i) => ( + +
+ {rs.gameName || "Unknown game"} +
+ + {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()} + +
+
+ ))} +
+ )} + + + {/* ── MangoHud Config ────────────────────────────────────── */} + + + +
+ + Write Config +
+
+
+ {configWritten && ( + +
+ Config written +
+
+ )} + + +
+ + Verify Config +
+
+
+ {configVerified.checked && ( + +
+ {configVerified.valid ? : } {configVerified.message} +
+
+ )} +
+ + {/* ── Configuration ────────────────────────────────────────── */} + + + onUpdateSetting("apiKey", e.target.value)} + placeholder="dv_..." + bIsPassword + /> + + + onUpdateSetting("exportPath", e.target.value)} + placeholder="/home/deck/Downloads" + /> + + + onUpdateSetting("baseUrl", e.target.value)} + placeholder="https://deckyvault.xyz" + /> + + + onUpdateSetting("hardwareSlug", opt.data as string || null)} + /> + + + +
+ + Export Config to Downloads +
+
+
+ + +
+ + Import Config from Downloads +
+
+
+ {configStatus && ( + +
+ {configStatus.isError ? : } {configStatus.message} +
+
+ )} +
+ + {/* ── MangoHud Setup 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 + + +

+ Troubleshooting:
+ • Log file empty? Check MangoHud is enabled for the game and the config was written.
+ • Not attaching? Try adding mangohud %command% to Steam launch options explicitly. +
+
+
+ ) } \ No newline at end of file diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 69a071e..123a80d 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -9,12 +9,13 @@ import { } 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" import { readAndParseMangohudLog, clearMangohudLog, writeMangohudConfig, + startMangohudLogging, + stopMangohudLogging, getHardwareInfo, getOsVersion, getProtonVersion, @@ -71,16 +72,20 @@ function Content() { // ── Handle start recording ──────────────────────────────────── async function handleStart() { - // Write MangoHud config with autostart_log so logging begins immediately + // Write MangoHud config with logging settings await writeMangohudConfig() // Clear any previous log file await clearMangohudLog() + // Start MangoHud logging via mangohudctl + await startMangohudLogging() startRecording() } // ── Handle stop recording: parse log + read system info ──────── async function handleStop() { try { + // Stop MangoHud logging via mangohudctl + await stopMangohudLogging() stopRecording() // Parse the MangoHud log @@ -161,9 +166,6 @@ function Content() { onReset={reset} setError={setError} setGameName={setGameName} - /> - diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index fe8dddd..2225247 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -33,11 +33,21 @@ export const readAndParseMangohudLog = callable<[logPath?: string], { error?: string }>("read_and_parse_mangohud_log") -export const clearMangohudLog = callable<[logPath?: string], { +export const clearMangohudLog = callable<[], { success: boolean error?: string }>("clear_mangohud_log") +export const startMangohudLogging = callable<[], { + success: boolean + error?: string +}>("start_mangohud_logging") + +export const stopMangohudLogging = callable<[], { + success: boolean + error?: string +}>("stop_mangohud_logging") + // ── System Info ───────────────────────────────────────────────── export const getHardwareInfo = callable<[], { slug: string From ea3b01b28b0d33b5c7a85d07fc4deb085ee9d804 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:23:58 +0800 Subject: [PATCH 09/30] fix(plugin): add User-Agent header to API requests for Cloudflare compat --- plugins/decky-vault/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index f83b223..c174684 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -465,7 +465,10 @@ benchmark_percentiles=97,AVG,1,0.1 url = f"{base_url}/api/games/lookup?steamAppId=0" req = urllib.request.Request( url, - headers={"x-api-key": api_key}, + headers={ + "x-api-key": api_key, + "User-Agent": "DeckyVaultPlugin/0.1", + }, method="GET" ) context = _get_ssl_context() From f61227e7fc1c524ab118d22c32fb90af8a511fef Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:25:23 +0800 Subject: [PATCH 10/30] fix(plugin): move Recording to top, clean up setup guide layout --- .../decky-vault/src/components/main-panel.tsx | 176 +++++++++--------- 1 file changed, 83 insertions(+), 93 deletions(-) diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index d80fcb1..4774ca8 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -212,77 +212,6 @@ export default function MainPanel({ return ( <> - {/* ── Status ──────────────────────────────────────────────── */} - - - -
- - Check MangoHud Status -
-
-
- {mangohudStatus.checked && ( - -
- {mangohudStatus.installed ? ( - <> MangoHud {mangohudStatus.version} - ) : ( - <> MangoHud not found - )} -
-
- )} - - - {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} - {keyTestStatus === "valid" && } - {keyTestStatus === "invalid" && } - - - {keyTestMessage && ( - -
- {keyTestMessage} -
-
- )} -
- - {/* ── Usage Instructions ──────────────────────────────────── */} - - -
- Add this to your game's Steam launch options, then press Start Recording before launching. -
-
- -
- mangohud %command% -
-
- - -
- - {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"} -
-
-
- -
- The MangoHud config is stored in ~/.config/MangoHud/MangoHud.conf -
-
-
- {/* ── Recording ──────────────────────────────────────────── */} {error && ( @@ -353,6 +282,77 @@ export default function MainPanel({ )} + {/* ── Status ──────────────────────────────────────────────── */} + + + +
+ + Check MangoHud Status +
+
+
+ {mangohudStatus.checked && ( + +
+ {mangohudStatus.installed ? ( + <> MangoHud {mangohudStatus.version} + ) : ( + <> MangoHud not found + )} +
+
+ )} + + + {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} + {keyTestStatus === "valid" && } + {keyTestStatus === "invalid" && } + + + {keyTestMessage && ( + +
+ {keyTestMessage} +
+
+ )} +
+ + {/* ── Usage Instructions ──────────────────────────────────── */} + + +
+ Add this to your game's Steam launch options, then press Start Recording before launching. +
+
+ +
+ mangohud %command% +
+
+ + +
+ + {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"} +
+
+
+ +
+ Config stored in ~/.config/MangoHud/MangoHud.conf +
+
+
+ {/* ── MangoHud Config ────────────────────────────────────── */} @@ -451,28 +451,18 @@ export default function MainPanel({
- 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 - - -

- Troubleshooting:
- • Log file empty? Check MangoHud is enabled for the game and the config was written.
- • Not attaching? Try adding mangohud %command% to Steam launch options explicitly. + Steam Deck (SteamOS): MangoHud is pre-installed. Add mangohud %command% to your game's Steam launch options (right-click → Properties → Launch Options). +
+
+ +
+ Other Linux: Install via sudo apt install mangohud or flatpak install ...VulkanLayer.MangoHud. See{" "} + github.com/flightlessmango/MangoHud. +
+
+ +
+ Troubleshooting: Log empty? Check MangoHud is enabled. Not attaching? Add mangohud %command% to launch options explicitly.
From aa52e0feb6a56f07b210710025fac73203c4db70 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:29:38 +0800 Subject: [PATCH 11/30] fix(plugin): use autostart_log instead of mangohudctl, improve log search --- plugins/decky-vault/main.py | 33 +++++++++++++++---------------- plugins/decky-vault/src/index.tsx | 8 +------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index c174684..12747a1 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -186,12 +186,11 @@ class Plugin: os.makedirs(config_dir, exist_ok=True) # MangoHud config that enables logging with the metrics we need. - # output_folder is required for logging to work. - # We log to /tmp so the plugin can read it after the session. + # autostart_log starts logging immediately when MangoHud initializes. config_content = """\ # DeckyVault MangoHud logging config output_folder=/tmp -output_file=deckyvault-mangohud.log +autostart_log=0 fps frame_timing cpu_power @@ -254,21 +253,20 @@ benchmark_percentiles=97,AVG,1,0.1 """Find the most recent MangoHud log file in /tmp/. MangoHud creates log files with the game name and timestamp.""" import glob - # Look for any CSV or log files in /tmp that might be MangoHud logs candidates = [] - for pattern in ["/tmp/*.csv", "/tmp/*.log", "/tmp/MangoHud*"]: + # MangoHud log files are typically .csv or have MangoHud in the name + for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: for f in glob.glob(pattern): - # Skip our own known file - if "deckyvault" in f: - candidates.append(f) + # Skip directories + if os.path.isdir(f): continue - # Check if the file starts with a MangoHud header + # Check if it looks like a MangoHud log (has fps/frametime header) try: with open(f, 'r') as fh: - first_line = fh.readline() - if 'MangoHud' in first_line or 'fps' in first_line.lower(): + first_lines = "".join(fh.readline() for _ in range(5)) + if 'fps' in first_lines.lower() or 'MangoHud' in first_lines: candidates.append(f) - except (IOError, UnicodeDecodeError): + except (IOError, UnicodeDecodeError, PermissionError): pass if not candidates: return None @@ -299,12 +297,13 @@ benchmark_percentiles=97,AVG,1,0.1 """RPC: Delete all MangoHud log files in /tmp/ so the next recording starts fresh.""" import glob try: - for pattern in ["/tmp/*.csv", "/tmp/*.log", "/tmp/MangoHud*"]: + for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: for f in glob.glob(pattern): - try: - os.remove(f) - except (IOError, PermissionError): - pass + if os.path.isfile(f): + try: + os.remove(f) + except (IOError, PermissionError): + pass return {"success": True} except Exception as e: return {"success": False, "error": str(e)} diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 123a80d..7167c55 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -14,8 +14,6 @@ import { readAndParseMangohudLog, clearMangohudLog, writeMangohudConfig, - startMangohudLogging, - stopMangohudLogging, getHardwareInfo, getOsVersion, getProtonVersion, @@ -72,20 +70,16 @@ function Content() { // ── Handle start recording ──────────────────────────────────── async function handleStart() { - // Write MangoHud config with logging settings + // Write MangoHud config with autostart_log so logging begins on game launch await writeMangohudConfig() // Clear any previous log file await clearMangohudLog() - // Start MangoHud logging via mangohudctl - await startMangohudLogging() startRecording() } // ── Handle stop recording: parse log + read system info ──────── async function handleStop() { try { - // Stop MangoHud logging via mangohudctl - await stopMangohudLogging() stopRecording() // Parse the MangoHud log From 37cc24ff60bb9bbf916b70e98ee7587af187fc78 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:33:12 +0800 Subject: [PATCH 12/30] fix(plugin): add control socket, autostart_log=1, timeout-safe mangohudctl --- plugins/decky-vault/main.py | 14 ++++++-------- plugins/decky-vault/src/index.tsx | 3 +++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 12747a1..8a01439 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -190,7 +190,8 @@ class Plugin: config_content = """\ # DeckyVault MangoHud logging config output_folder=/tmp -autostart_log=0 +control=mangohud +autostart_log=1 fps frame_timing cpu_power @@ -233,20 +234,17 @@ benchmark_percentiles=97,AVG,1,0.1 return {"success": False, "error": str(e)} async def stop_mangohud_logging(self) -> dict: - """RPC: Stop MangoHud logging via mangohudctl.""" + """RPC: Stop MangoHud logging via mangohudctl. Best-effort, may fail if game already closed.""" import subprocess try: result = subprocess.run( ["mangohudctl", "set", "log_session", "false"], - capture_output=True, text=True, timeout=5 + capture_output=True, text=True, timeout=2 ) if result.returncode == 0: return {"success": True} - else: - return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} - except FileNotFoundError: - return {"success": False, "error": "mangohudctl not found. Is MangoHud running?"} - except Exception as e: + return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} + except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e: return {"success": False, "error": str(e)} async def _find_mangohud_log(self) -> str | None: diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index 7167c55..a26c72c 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -14,6 +14,7 @@ import { readAndParseMangohudLog, clearMangohudLog, writeMangohudConfig, + stopMangohudLogging, getHardwareInfo, getOsVersion, getProtonVersion, @@ -80,6 +81,8 @@ function Content() { // ── Handle stop recording: parse log + read system info ──────── async function handleStop() { try { + // Try to stop MangoHud logging (best-effort, may fail if game already closed) + await stopMangohudLogging() stopRecording() // Parse the MangoHud log From 928ec8ec9a0855a9f2e0943ee76e50a6e458a456 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:38:18 +0800 Subject: [PATCH 13/30] fix(plugin): retry mangohudctl start logging until game launches --- plugins/decky-vault/main.py | 23 +++++++++++++++++++++-- plugins/decky-vault/src/index.tsx | 5 ++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 8a01439..50ccbcd 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -233,8 +233,27 @@ benchmark_percentiles=97,AVG,1,0.1 except Exception as e: return {"success": False, "error": str(e)} + async def start_mangohud_logging(self) -> dict: + """RPC: Start MangoHud logging via mangohudctl. + Retries a few times in case MangoHud hasn't started yet.""" + import subprocess + import time + for attempt in range(5): + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "true"], + capture_output=True, text=True, timeout=2 + ) + if result.returncode == 0: + return {"success": True} + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + if attempt < 4: + await asyncio.sleep(2) + return {"success": False, "error": "Could not connect to MangoHud. Is the game running?"} + async def stop_mangohud_logging(self) -> dict: - """RPC: Stop MangoHud logging via mangohudctl. Best-effort, may fail if game already closed.""" + """RPC: Stop MangoHud logging via mangohudctl. Best-effort.""" import subprocess try: result = subprocess.run( @@ -244,7 +263,7 @@ benchmark_percentiles=97,AVG,1,0.1 if result.returncode == 0: return {"success": True} return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} - except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e: + except Exception as e: return {"success": False, "error": str(e)} async def _find_mangohud_log(self) -> str | None: diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx index a26c72c..55a0e97 100644 --- a/plugins/decky-vault/src/index.tsx +++ b/plugins/decky-vault/src/index.tsx @@ -14,6 +14,7 @@ import { readAndParseMangohudLog, clearMangohudLog, writeMangohudConfig, + startMangohudLogging, stopMangohudLogging, getHardwareInfo, getOsVersion, @@ -71,10 +72,12 @@ function Content() { // ── Handle start recording ──────────────────────────────────── async function handleStart() { - // Write MangoHud config with autostart_log so logging begins on game launch + // Write MangoHud config with logging settings await writeMangohudConfig() // Clear any previous log file await clearMangohudLog() + // Fire-and-forget: try to start MangoHud logging (retries until game launches) + startMangohudLogging() startRecording() } From e1269b2de1970e2861285049ee240f921ff8bfeb Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:40:32 +0800 Subject: [PATCH 14/30] fix(plugin): clean up MangoHud version display --- plugins/decky-vault/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 50ccbcd..def7989 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -170,6 +170,9 @@ class Plugin: capture_output=True, text=True, timeout=5 ) version = version_result.stdout.strip() if version_result.returncode == 0 else "unknown" + # Clean up version string (remove git hash suffix) + if version and "-" in version: + version = version.split("-")[0] return {"installed": True, "path": mangohud_path, "version": version} else: return {"installed": False, "path": "", "version": ""} From 912b783593d9d36b25bcb082859be679b3447758 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:43:37 +0800 Subject: [PATCH 15/30] fix(plugin): fallback version check with full binary path --- plugins/decky-vault/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index def7989..220f9ad 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -173,6 +173,14 @@ class Plugin: # Clean up version string (remove git hash suffix) if version and "-" in version: version = version.split("-")[0] + # Fallback: try with full path if first attempt failed + if version == "unknown" and mangohud_path: + try: + v2 = subprocess.run([mangohud_path, "--version"], capture_output=True, text=True, timeout=5) + if v2.returncode == 0: + version = v2.stdout.strip().split("-")[0] + except: + pass return {"installed": True, "path": mangohud_path, "version": version} else: return {"installed": False, "path": "", "version": ""} From 6b2dada0827c9a35f941bbb95df042cffa623748 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:45:28 +0800 Subject: [PATCH 16/30] fix(plugin): use full path for mangohud, return empty string instead of unknown --- plugins/decky-vault/main.py | 45 +++++++++++++++---------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 220f9ad..ae219f5 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -156,34 +156,25 @@ class Plugin: async def check_mangohud(self) -> dict: """RPC: Check if MangoHud is installed. Returns {installed: bool, path: str, version: str}.""" - import subprocess + import subprocess as sp + import os try: - result = subprocess.run( - ["which", "mangohud"], - capture_output=True, text=True, timeout=5 - ) - if result.returncode == 0: - mangohud_path = result.stdout.strip() - # Get version - version_result = subprocess.run( - ["mangohud", "--version"], - capture_output=True, text=True, timeout=5 - ) - version = version_result.stdout.strip() if version_result.returncode == 0 else "unknown" - # Clean up version string (remove git hash suffix) - if version and "-" in version: - version = version.split("-")[0] - # Fallback: try with full path if first attempt failed - if version == "unknown" and mangohud_path: - try: - v2 = subprocess.run([mangohud_path, "--version"], capture_output=True, text=True, timeout=5) - if v2.returncode == 0: - version = v2.stdout.strip().split("-")[0] - except: - pass - return {"installed": True, "path": mangohud_path, "version": version} - else: - return {"installed": False, "path": "", "version": ""} + # Use the full path to avoid PATH issues + mangohud_path = "/usr/bin/mangohud" + if not os.path.exists(mangohud_path): + # Fall back to which + r = sp.run(["which", "mangohud"], capture_output=True, text=True, timeout=5) + if r.returncode != 0: + return {"installed": False, "path": "", "version": ""} + mangohud_path = r.stdout.strip() + + # Get version using the full path + v = sp.run([mangohud_path, "--version"], capture_output=True, text=True, timeout=5) + version = v.stdout.strip() if v.returncode == 0 else "" + if version and "-" in version: + version = version.split("-")[0] + + return {"installed": True, "path": mangohud_path, "version": version or ""} except Exception as e: return {"installed": False, "path": "", "version": "", "error": str(e)} From 55bc86874a965a654ae342c703fe8af6ae8042f5 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:46:42 +0800 Subject: [PATCH 17/30] fix(plugin): use os.popen instead of subprocess for mangohud version --- plugins/decky-vault/main.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index ae219f5..d6b16d0 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -155,26 +155,26 @@ class Plugin: return self._settings async def check_mangohud(self) -> dict: - """RPC: Check if MangoHud is installed. Returns {installed: bool, path: str, version: str}.""" - import subprocess as sp + """RPC: Check if MangoHud is installed.""" import os try: - # Use the full path to avoid PATH issues - mangohud_path = "/usr/bin/mangohud" - if not os.path.exists(mangohud_path): - # Fall back to which - r = sp.run(["which", "mangohud"], capture_output=True, text=True, timeout=5) - if r.returncode != 0: - return {"installed": False, "path": "", "version": ""} - mangohud_path = r.stdout.strip() + path = "/usr/bin/mangohud" + exists = os.path.exists(path) + if not exists: + return {"installed": False, "path": "", "version": ""} - # Get version using the full path - v = sp.run([mangohud_path, "--version"], capture_output=True, text=True, timeout=5) - version = v.stdout.strip() if v.returncode == 0 else "" - if version and "-" in version: - version = version.split("-")[0] + # Get version using os.popen (more reliable than subprocess in some envs) + version = "" + try: + with os.popen(f"{path} --version 2>/dev/null") as pipe: + v = pipe.read().strip() + if v and "-" in v: + v = v.split("-")[0] + version = v + except: + pass - return {"installed": True, "path": mangohud_path, "version": version or ""} + return {"installed": True, "path": path, "version": version} except Exception as e: return {"installed": False, "path": "", "version": "", "error": str(e)} From 6a486ed25709138df111a65e55b139fc47c2d68e Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:47:43 +0800 Subject: [PATCH 18/30] fix(plugin): add debug logging to check_mangohud --- plugins/decky-vault/main.py | 23 +++++++++++-------- .../decky-vault/src/components/main-panel.tsx | 3 +++ plugins/decky-vault/src/lib/api.ts | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index d6b16d0..48e6b2d 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -161,22 +161,25 @@ class Plugin: path = "/usr/bin/mangohud" exists = os.path.exists(path) if not exists: - return {"installed": False, "path": "", "version": ""} + return {"installed": False, "path": "", "version": "", "debug": "file not found"} - # Get version using os.popen (more reliable than subprocess in some envs) + # Get version using os.popen version = "" + debug = "" try: - with os.popen(f"{path} --version 2>/dev/null") as pipe: + with os.popen(f"{path} --version 2>&1") as pipe: v = pipe.read().strip() - if v and "-" in v: - v = v.split("-")[0] - version = v - except: - pass + debug = f"popen got: {repr(v)}" + if v: + if "-" in v: + v = v.split("-")[0] + version = v + except Exception as e: + debug = f"popen error: {str(e)}" - return {"installed": True, "path": path, "version": version} + return {"installed": True, "path": path, "version": version, "debug": debug} except Exception as e: - return {"installed": False, "path": "", "version": "", "error": str(e)} + return {"installed": False, "path": "", "version": "", "error": str(e), "debug": "outer error"} async def write_mangohud_config(self) -> dict: """RPC: Write the MangoHud logging config to ~/.config/MangoHud/MangoHud.conf. diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 4774ca8..771cecc 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -112,6 +112,9 @@ export default function MainPanel({ path: result.path, version: result.version, }) + if (result.debug) { + console.log("[DeckyVault] MangoHud debug:", result.debug) + } } async function handleTestKey() { diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index 2225247..e5340f0 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -10,6 +10,7 @@ export const checkMangohud = callable<[], { path: string version: string error?: string + debug?: string }>("check_mangohud") export const writeMangohudConfig = callable<[], { From 2b53b30be635197faa1d3e2dce90117d3082a2e7 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:48:27 +0800 Subject: [PATCH 19/30] fix(plugin): read mangohud version from script file instead of running it --- plugins/decky-vault/main.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 48e6b2d..1d9e52c 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -157,29 +157,30 @@ class Plugin: async def check_mangohud(self) -> dict: """RPC: Check if MangoHud is installed.""" import os + import re try: path = "/usr/bin/mangohud" exists = os.path.exists(path) if not exists: - return {"installed": False, "path": "", "version": "", "debug": "file not found"} + return {"installed": False, "path": "", "version": ""} - # Get version using os.popen + # Read version from the shell script itself version = "" - debug = "" try: - with os.popen(f"{path} --version 2>&1") as pipe: - v = pipe.read().strip() - debug = f"popen got: {repr(v)}" - if v: - if "-" in v: - v = v.split("-")[0] - version = v - except Exception as e: - debug = f"popen error: {str(e)}" + with open(path, 'r') as f: + content = f.read() + # Look for the version line: echo v0.8.3-rc1-24-g33c2c7dd+ + m = re.search(r'echo\s+(v?[\d.]+[^\s]*)', content) + if m: + version = m.group(1) + if "-" in version: + version = version.split("-")[0] + except: + pass - return {"installed": True, "path": path, "version": version, "debug": debug} + return {"installed": True, "path": path, "version": version} except Exception as e: - return {"installed": False, "path": "", "version": "", "error": str(e), "debug": "outer error"} + return {"installed": False, "path": "", "version": "", "error": str(e)} async def write_mangohud_config(self) -> dict: """RPC: Write the MangoHud logging config to ~/.config/MangoHud/MangoHud.conf. From 7a653198e769520811e7ed2b72799834ad52c972 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:51:47 +0800 Subject: [PATCH 20/30] fix(plugin): update verify config check, add debug list tmp --- plugins/decky-vault/main.py | 36 ++++++++++++++----- .../decky-vault/src/components/main-panel.tsx | 4 +-- plugins/decky-vault/src/lib/api.ts | 4 +++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 1d9e52c..05da0b5 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -273,30 +273,50 @@ benchmark_percentiles=97,AVG,1,0.1 return {"success": False, "error": str(e)} async def _find_mangohud_log(self) -> str | None: - """Find the most recent MangoHud log file in /tmp/. - MangoHud creates log files with the game name and timestamp.""" + """Find the most recent MangoHud log file in /tmp/.""" import glob + import time candidates = [] - # MangoHud log files are typically .csv or have MangoHud in the name + now = time.time() for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: for f in glob.glob(pattern): - # Skip directories if os.path.isdir(f): continue - # Check if it looks like a MangoHud log (has fps/frametime header) + # Only consider files modified in the last hour + try: + mtime = os.path.getmtime(f) + if now - mtime > 3600: + continue + except: + pass + # Check if it looks like a MangoHud log try: with open(f, 'r') as fh: first_lines = "".join(fh.readline() for _ in range(5)) if 'fps' in first_lines.lower() or 'MangoHud' in first_lines: candidates.append(f) - except (IOError, UnicodeDecodeError, PermissionError): - pass + except: + candidates.append(f) # Add anyway if we can't read it if not candidates: return None - # Return the most recently modified file candidates.sort(key=lambda f: os.path.getmtime(f), reverse=True) return candidates[0] + async def debug_list_tmp(self) -> dict: + """RPC: List all files in /tmp/ for debugging.""" + import glob + files = [] + for f in glob.glob("/tmp/*"): + if os.path.isfile(f): + try: + mtime = os.path.getmtime(f) + size = os.path.getsize(f) + files.append({"name": os.path.basename(f), "size": size, "mtime": mtime}) + except: + pass + files.sort(key=lambda x: x["mtime"], reverse=True) + return {"files": files[:30]} + async def read_and_parse_mangohud_log(self, log_path: str | None = None) -> dict: """RPC: Read the MangoHud log file and return parsed FPS stats. If no log_path given, searches /tmp/ for the most recent MangoHud log. diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 771cecc..4d90050 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -165,9 +165,9 @@ export default function MainPanel({ } 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") - if (hasOutputFolder && hasOutputFile && hasFps) { + const hasFrameTiming = content.includes("frame_timing") + if (hasOutputFolder && hasFps) { setConfigVerified({ checked: true, valid: true, message: "Config looks good" }) } else { setConfigVerified({ checked: true, valid: false, message: "Config is missing required settings. Write it again." }) diff --git a/plugins/decky-vault/src/lib/api.ts b/plugins/decky-vault/src/lib/api.ts index e5340f0..4b1f005 100644 --- a/plugins/decky-vault/src/lib/api.ts +++ b/plugins/decky-vault/src/lib/api.ts @@ -86,6 +86,10 @@ export const testApiKey = callable<[apiKey: string, baseUrl?: string], { error?: string }>("test_api_key") +export const debugListTmp = callable<[], { + files: Array<{ name: string; size: number; mtime: number }> +}>("debug_list_tmp") + // ── Config Export/Import ──────────────────────────────────────── export const exportConfig = callable<[settings: { apiKey: string From b98528fbaf5cd5c36520e4ac7b8fa4b96f5abdf3 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:54:01 +0800 Subject: [PATCH 21/30] fix(plugin): add wrapper script to force MangoHud config file --- plugins/decky-vault/main.py | 18 +++++++++++++----- .../decky-vault/src/components/main-panel.tsx | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 05da0b5..70451c2 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -183,16 +183,13 @@ class Plugin: return {"installed": False, "path": "", "version": "", "error": str(e)} async def write_mangohud_config(self) -> dict: - """RPC: Write the MangoHud logging config to ~/.config/MangoHud/MangoHud.conf. - Returns {success: bool, path: str, error: str?}.""" + """RPC: Write the MangoHud logging config and a wrapper script.""" try: home = os.path.expanduser("~") config_dir = os.path.join(home, ".config", "MangoHud") config_path = os.path.join(config_dir, "MangoHud.conf") os.makedirs(config_dir, exist_ok=True) - # MangoHud config that enables logging with the metrics we need. - # autostart_log starts logging immediately when MangoHud initializes. config_content = """\ # DeckyVault MangoHud logging config output_folder=/tmp @@ -209,7 +206,18 @@ benchmark_percentiles=97,AVG,1,0.1 with open(config_path, 'w') as f: f.write(config_content) - return {"success": True, "path": config_path} + # Write a wrapper script that forces MangoHud to use our config + wrapper_path = os.path.join(home, "deckyvault-mangohud.sh") + wrapper_content = """\ +#!/bin/bash +export MANGOHUD_CONFIGFILE="$HOME/.config/MangoHud/MangoHud.conf" +exec mangohud "$@" +""" + with open(wrapper_path, 'w') as f: + f.write(wrapper_content) + os.chmod(wrapper_path, 0o755) + + return {"success": True, "path": config_path, "wrapper": wrapper_path} except Exception as e: return {"success": False, "path": "", "error": str(e)} diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 4d90050..7e57942 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -338,7 +338,7 @@ export default function MainPanel({ fontSize: "14px", textAlign: "center", }}> - mangohud %command% + ~/deckyvault-mangohud.sh %command%
From e3c3ed54c65c8494d5bda0001cc7d2d8cf8e7edc Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:56:40 +0800 Subject: [PATCH 22/30] fix(plugin): update copy button to use wrapper script path --- plugins/decky-vault/src/components/main-panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 7e57942..3c907cb 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -137,12 +137,12 @@ export default function MainPanel({ async function handleCopyLaunchOption() { try { - await navigator.clipboard.writeText("mangohud %command%") + await navigator.clipboard.writeText("~/deckyvault-mangohud.sh %command%") setCopiedLaunchOpt(true) setTimeout(() => setCopiedLaunchOpt(false), 2000) } catch { const ta = document.createElement("textarea") - ta.value = "mangohud %command%" + ta.value = "~/deckyvault-mangohud.sh %command%" document.body.appendChild(ta) ta.select() document.execCommand("copy") From ec1d4a074619bbe897989efc8d0c8b73829ad7fb Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 21:01:53 +0800 Subject: [PATCH 23/30] fix(plugin): remove in-game settings, single-line notes, record-while-in-game flow --- plugins/decky-vault/src/components/main-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 3c907cb..7f3e88c 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -326,7 +326,7 @@ export default function MainPanel({
- Add this to your game's Steam launch options, then press Start Recording before launching. + Add this to your game's Steam launch options, then launch the game. Press Start Recording once you're in-game and ready to benchmark.
From 7ebb2669a1fd493cc2c0dbae943dcac64f311c88 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 21:03:00 +0800 Subject: [PATCH 24/30] fix(plugin): sum cpu_power + gpu_power for TDP, remove in-game settings, single-line notes --- plugins/decky-vault/main.py | 19 +++++++++++++++++-- .../src/components/session-form.tsx | 12 ------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 70451c2..1f3a913 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -34,6 +34,7 @@ def parse_mangohud_log(log_content: str) -> dict: fps_col = 0 frametime_col = None gpu_power_col = None + cpu_power_col = None for i, line in enumerate(lines): stripped = line.strip() @@ -47,6 +48,8 @@ def parse_mangohud_log(log_content: str) -> dict: frametime_col = columns.index('frametime') if 'gpu_power' in columns: gpu_power_col = columns.index('gpu_power') + if 'cpu_power' in columns: + cpu_power_col = columns.index('cpu_power') header_idx = i break @@ -56,6 +59,7 @@ def parse_mangohud_log(log_content: str) -> dict: fps_values = [] frametime_values = [] gpu_power_values = [] + cpu_power_values = [] for line in lines[header_idx + 1:]: stripped = line.strip() @@ -71,6 +75,8 @@ def parse_mangohud_log(log_content: str) -> dict: frametime_values.append(float(parts[frametime_col])) if gpu_power_col is not None and gpu_power_col < len(parts): gpu_power_values.append(float(parts[gpu_power_col])) + if cpu_power_col is not None and cpu_power_col < len(parts): + cpu_power_values.append(float(parts[cpu_power_col])) except (ValueError, IndexError): continue @@ -96,8 +102,17 @@ def parse_mangohud_log(log_content: str) -> dict: fps_one_percent_low = round(sorted_fps[one_percent_idx], 1) tdp_watts = None - if gpu_power_values: - tdp_watts = round(sum(gpu_power_values) / len(gpu_power_values), 1) + total_power_values = [] + if gpu_power_values and cpu_power_values: + # Sum GPU and CPU power for total APU power + for gp, cp in zip(gpu_power_values, cpu_power_values): + total_power_values.append(gp + cp) + elif gpu_power_values: + total_power_values = gpu_power_values + elif cpu_power_values: + total_power_values = cpu_power_values + if total_power_values: + tdp_watts = round(sum(total_power_values) / len(total_power_values), 1) return { "fpsAvg": fps_avg, diff --git a/plugins/decky-vault/src/components/session-form.tsx b/plugins/decky-vault/src/components/session-form.tsx index 092cf8c..7e6dadf 100644 --- a/plugins/decky-vault/src/components/session-form.tsx +++ b/plugins/decky-vault/src/components/session-form.tsx @@ -156,18 +156,6 @@ export default function SessionForm({ /> - - -