From 6e1957524b9e404693debab05b6cb1320bfaec51 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 20:21:33 +0800 Subject: [PATCH] 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