summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/image.pngbin1232759 -> 0 bytes
-rw-r--r--main.py461
-rw-r--r--src/api/lsfgApi.ts60
-rw-r--r--src/components/ConfigurationSection.tsx115
-rw-r--r--src/components/Content.tsx108
-rw-r--r--src/components/InstallationButton.tsx64
-rw-r--r--src/components/StatusDisplay.tsx38
-rw-r--r--src/components/UsageInstructions.tsx68
-rw-r--r--src/components/index.ts5
-rw-r--r--src/hooks/useInstallationActions.ts89
-rw-r--r--src/hooks/useLsfgHooks.ts156
-rwxr-xr-xsrc/index.tsx450
-rw-r--r--src/plugin_lifecycle.py47
-rw-r--r--src/services/__init__.py5
-rw-r--r--src/services/configuration_service.py164
-rw-r--r--src/services/dll_detection_service.py60
-rw-r--r--src/services/installation_service.py205
17 files changed, 1224 insertions, 871 deletions
diff --git a/assets/image.png b/assets/image.png
deleted file mode 100644
index 51680a3..0000000
--- a/assets/image.png
+++ /dev/null
Binary files differ
diff --git a/main.py b/main.py
index 96845fa..c4578ae 100644
--- a/main.py
+++ b/main.py
@@ -10,455 +10,62 @@ import tempfile
import decky
import asyncio
+# Import our modular services
+from src.services import InstallationService, DllDetectionService, ConfigurationService
+from src.plugin_lifecycle import PluginLifecycleManager
+
+
class Plugin:
+ def __init__(self):
+ # Initialize services
+ self.installation_service = InstallationService()
+ self.dll_detection_service = DllDetectionService()
+ self.configuration_service = ConfigurationService()
+ self.lifecycle_manager = PluginLifecycleManager()
+
+ # Installation methods
async def install_lsfg_vk(self) -> dict:
"""Install lsfg-vk by extracting the zip file to ~/.local"""
- try:
- # Get the path to the lsfg-vk_archlinux.zip file in the bin directory
- plugin_dir = os.path.dirname(os.path.realpath(__file__))
- zip_path = os.path.join(plugin_dir, "bin", "lsfg-vk_archlinux.zip")
-
- # Check if the zip file exists
- if not os.path.exists(zip_path):
- decky.logger.error(f"lsfg-vk_archlinux.zip not found at {zip_path}")
- return {"success": False, "error": "lsfg-vk_archlinux.zip file not found"}
-
- # Get the user's home directory
- user_home = os.path.expanduser("~")
- local_lib_dir = os.path.join(user_home, ".local", "lib")
- local_share_dir = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d")
-
- # Create directories if they don't exist
- os.makedirs(local_lib_dir, exist_ok=True)
- os.makedirs(local_share_dir, exist_ok=True)
-
- # Extract the zip file
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
- # Use /tmp for temporary extraction since we may not have write permissions in plugin dir
- with tempfile.TemporaryDirectory() as temp_dir:
- zip_ref.extractall(temp_dir)
-
- # Look for the extracted files and copy them to the correct locations
- for root, dirs, files in os.walk(temp_dir):
- for file in files:
- src_file = os.path.join(root, file)
- if file.endswith('.so'):
- # Copy library files to ~/.local/lib
- dst_file = os.path.join(local_lib_dir, file)
- shutil.copy2(src_file, dst_file)
- decky.logger.info(f"Copied {file} to {dst_file}")
- elif file.endswith('.json'):
- # Copy JSON files to ~/.local/share/vulkan/implicit_layer.d
- dst_file = os.path.join(local_share_dir, file)
- shutil.copy2(src_file, dst_file)
- decky.logger.info(f"Copied {file} to {dst_file}")
-
- # temp_dir will be automatically cleaned up
-
- # Create the lsfg script in home directory
- lsfg_script_path = os.path.join(user_home, "lsfg")
- script_content = """#!/bin/bash
-
-export ENABLE_LSFG=1
-export LSFG_MULTIPLIER=2
-export LSFG_FLOW_SCALE=1.0
-# export LSFG_HDR=1
-# export LSFG_PERF_MODE=1
-# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync
-
-# Execute the passed command with the environment variables set
-exec "$@"
-"""
-
- with open(lsfg_script_path, 'w') as script_file:
- script_file.write(script_content)
-
- # Make the script executable
- os.chmod(lsfg_script_path, 0o755)
- decky.logger.info(f"Created executable lsfg script at {lsfg_script_path}")
-
- decky.logger.info("lsfg-vk installed successfully")
- return {"success": True, "message": "lsfg-vk installed successfully"}
-
- except Exception as e:
- decky.logger.error(f"Error installing lsfg-vk: {str(e)}")
- return {"success": False, "error": str(e)}
+ return await self.installation_service.install()
async def check_lsfg_vk_installed(self) -> dict:
"""Check if lsfg-vk is already installed"""
- try:
- user_home = os.path.expanduser("~")
- lib_file = os.path.join(user_home, ".local", "lib", "liblsfg-vk.so")
- json_file = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d", "VkLayer_LS_frame_generation.json")
- lsfg_script = os.path.join(user_home, "lsfg")
-
- lib_exists = os.path.exists(lib_file)
- json_exists = os.path.exists(json_file)
- script_exists = os.path.exists(lsfg_script)
-
- decky.logger.info(f"Installation check: lib={lib_exists}, json={json_exists}, script={script_exists}")
-
- return {
- "installed": lib_exists and json_exists,
- "lib_exists": lib_exists,
- "json_exists": json_exists,
- "script_exists": script_exists,
- "lib_path": lib_file,
- "json_path": json_file,
- "script_path": lsfg_script
- }
-
- except Exception as e:
- decky.logger.error(f"Error checking lsfg-vk installation: {str(e)}")
- return {"installed": False, "error": str(e)}
+ return await self.installation_service.check_installation()
async def uninstall_lsfg_vk(self) -> dict:
"""Uninstall lsfg-vk by removing the installed files"""
- try:
- user_home = os.path.expanduser("~")
- lib_file = os.path.join(user_home, ".local", "lib", "liblsfg-vk.so")
- json_file = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d", "VkLayer_LS_frame_generation.json")
- lsfg_script = os.path.join(user_home, "lsfg")
-
- removed_files = []
-
- # Remove library file if it exists
- if os.path.exists(lib_file):
- os.remove(lib_file)
- removed_files.append(lib_file)
- decky.logger.info(f"Removed {lib_file}")
-
- # Remove JSON file if it exists
- if os.path.exists(json_file):
- os.remove(json_file)
- removed_files.append(json_file)
- decky.logger.info(f"Removed {json_file}")
-
- # Remove lsfg script if it exists
- if os.path.exists(lsfg_script):
- os.remove(lsfg_script)
- removed_files.append(lsfg_script)
- decky.logger.info(f"Removed {lsfg_script}")
-
- if not removed_files:
- return {"success": True, "message": "No lsfg-vk files found to remove"}
-
- decky.logger.info("lsfg-vk uninstalled successfully")
- return {
- "success": True,
- "message": f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.",
- "removed_files": removed_files
- }
-
- except Exception as e:
- decky.logger.error(f"Error uninstalling lsfg-vk: {str(e)}")
- return {"success": False, "error": str(e)}
+ return await self.installation_service.uninstall()
+ # DLL detection methods
async def check_lossless_scaling_dll(self) -> dict:
"""Check if Lossless Scaling DLL is available at the expected paths"""
- try:
- # Check environment variable first
- dll_path = os.getenv("LSFG_DLL_PATH")
- if dll_path and dll_path.strip():
- dll_path_str = dll_path.strip()
- if os.path.exists(dll_path_str):
- return {
- "detected": True,
- "path": dll_path_str,
- "source": "LSFG_DLL_PATH environment variable"
- }
-
- # Check XDG_DATA_HOME path
- data_dir = os.getenv("XDG_DATA_HOME")
- if data_dir and data_dir.strip():
- dll_path_str = os.path.join(data_dir.strip(), "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll")
- if os.path.exists(dll_path_str):
- return {
- "detected": True,
- "path": dll_path_str,
- "source": "XDG_DATA_HOME Steam directory"
- }
-
- # Check HOME/.local/share path
- home_dir = os.getenv("HOME")
- if home_dir and home_dir.strip():
- dll_path_str = os.path.join(home_dir.strip(), ".local", "share", "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll")
- if os.path.exists(dll_path_str):
- return {
- "detected": True,
- "path": dll_path_str,
- "source": "HOME/.local/share Steam directory"
- }
-
- # DLL not found in any expected location
- return {
- "detected": False,
- "path": None,
- "source": None,
- "message": "Lossless Scaling DLL not found in expected locations"
- }
-
- except Exception as e:
- decky.logger.error(f"Error checking Lossless Scaling DLL: {str(e)}")
- return {
- "detected": False,
- "path": None,
- "source": None,
- "error": str(e)
- }
+ return await self.dll_detection_service.check_lossless_scaling_dll()
+ # Configuration methods
async def get_lsfg_config(self) -> dict:
"""Read current lsfg script configuration"""
- try:
- user_home = os.path.expanduser("~")
- lsfg_script_path = os.path.join(user_home, "lsfg")
-
- if not os.path.exists(lsfg_script_path):
- return {
- "success": False,
- "error": "lsfg script not found"
- }
-
- with open(lsfg_script_path, 'r') as f:
- content = f.read()
-
- # Parse the script content to extract current values
- config = {
- "enable_lsfg": False,
- "multiplier": 2,
- "flow_scale": 1.0,
- "hdr": False,
- "perf_mode": False,
- "immediate_mode": False
- }
-
- lines = content.split('\n')
- for line in lines:
- line = line.strip()
-
- # Handle ENABLE_LSFG - check if it's commented out or not
- if line.startswith('export ENABLE_LSFG='):
- try:
- value = line.split('=')[1].strip()
- config["enable_lsfg"] = value == '1'
- except:
- pass
- elif line.startswith('# export ENABLE_LSFG='):
- config["enable_lsfg"] = False
-
- # Handle LSFG_MULTIPLIER
- elif line.startswith('export LSFG_MULTIPLIER='):
- try:
- value = line.split('=')[1].strip()
- config["multiplier"] = int(value)
- except:
- pass
-
- # Handle LSFG_FLOW_SCALE
- elif line.startswith('export LSFG_FLOW_SCALE='):
- try:
- value = line.split('=')[1].strip()
- config["flow_scale"] = float(value)
- except:
- pass
-
- # Handle LSFG_HDR - check if it's commented out or not
- elif line.startswith('export LSFG_HDR='):
- try:
- value = line.split('=')[1].strip()
- config["hdr"] = value == '1'
- except:
- pass
- elif line.startswith('# export LSFG_HDR='):
- config["hdr"] = False
-
- # Handle LSFG_PERF_MODE - check if it's commented out or not
- elif line.startswith('export LSFG_PERF_MODE='):
- try:
- value = line.split('=')[1].strip()
- config["perf_mode"] = value == '1'
- except:
- pass
- elif line.startswith('# export LSFG_PERF_MODE='):
- config["perf_mode"] = False
-
- # Handle MESA_VK_WSI_PRESENT_MODE - check if it's commented out or not
- elif line.startswith('export MESA_VK_WSI_PRESENT_MODE='):
- try:
- value = line.split('=')[1].strip()
- # Remove any comments after the value
- value = value.split('#')[0].strip()
- config["immediate_mode"] = value == 'immediate'
- except:
- pass
- elif line.startswith('# export MESA_VK_WSI_PRESENT_MODE='):
- config["immediate_mode"] = False
-
- decky.logger.info(f"Parsed lsfg config: {config}")
-
- return {
- "success": True,
- "config": config
- }
-
- except Exception as e:
- decky.logger.error(f"Error reading lsfg config: {str(e)}")
- return {
- "success": False,
- "error": str(e)
- }
+ return await self.configuration_service.get_config()
- async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, hdr: bool, perf_mode: bool, immediate_mode: bool) -> dict:
+ async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float,
+ hdr: bool, perf_mode: bool, immediate_mode: bool) -> dict:
"""Update lsfg script configuration"""
- try:
- user_home = os.path.expanduser("~")
- lsfg_script_path = os.path.join(user_home, "lsfg")
-
- # Create script content based on parameters
- script_content = "#!/bin/bash\n\n"
-
- if enable_lsfg:
- script_content += "export ENABLE_LSFG=1\n"
- else:
- script_content += "# export ENABLE_LSFG=1\n"
-
- script_content += f"export LSFG_MULTIPLIER={multiplier}\n"
- script_content += f"export LSFG_FLOW_SCALE={flow_scale}\n"
-
- if hdr:
- script_content += "export LSFG_HDR=1\n"
- else:
- script_content += "# export LSFG_HDR=1\n"
-
- if perf_mode:
- script_content += "export LSFG_PERF_MODE=1\n"
- else:
- script_content += "# export LSFG_PERF_MODE=1\n"
-
- if immediate_mode:
- script_content += "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n"
- else:
- script_content += "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n"
-
- # Add the exec line to allow the script to execute passed commands
- script_content += "\n# Execute the passed command with the environment variables set\n"
- script_content += "exec \"$@\"\n"
-
- # Write the updated script
- with open(lsfg_script_path, 'w') as f:
- f.write(script_content)
-
- # Make sure it's executable
- os.chmod(lsfg_script_path, 0o755)
-
- decky.logger.info(f"Updated lsfg script configuration: enable={enable_lsfg}, multiplier={multiplier}, flow_scale={flow_scale}, hdr={hdr}, perf_mode={perf_mode}, immediate_mode={immediate_mode}")
-
- return {
- "success": True,
- "message": "lsfg configuration updated successfully"
- }
-
- except Exception as e:
- decky.logger.error(f"Error updating lsfg config: {str(e)}")
- return {
- "success": False,
- "error": str(e)
- }
+ return await self.configuration_service.update_config(
+ enable_lsfg, multiplier, flow_scale, hdr, perf_mode, immediate_mode
+ )
- # Asyncio-compatible long-running code, executed in a task when the plugin is loaded
+ # Plugin lifecycle methods
async def _main(self):
- decky.logger.info("Lossless Scaling loaded!")
+ """Asyncio-compatible long-running code, executed in a task when the plugin is loaded"""
+ await self.lifecycle_manager.on_load()
- # Function called first during the unload process, utilize this to handle your plugin being stopped, but not
- # completely removed
async def _unload(self):
- decky.logger.info("Lossless Scaling unloading")
- pass
+ """Function called first during the unload process"""
+ await self.lifecycle_manager.on_unload()
- # Function called after `_unload` during uninstall, utilize this to clean up processes and other remnants of your
- # plugin that may remain on the system
async def _uninstall(self):
- decky.logger.info("Lossless Scaling uninstalled - starting cleanup")
-
- # Clean up lsfg-vk files when the plugin is uninstalled
- try:
- user_home = os.path.expanduser("~")
- lib_file = os.path.join(user_home, ".local", "lib", "liblsfg-vk.so")
- json_file = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d", "VkLayer_LS_frame_generation.json")
- lsfg_script = os.path.join(user_home, "lsfg")
-
- decky.logger.info(f"Checking for lsfg-vk files to clean up:")
- decky.logger.info(f" Library file: {lib_file}")
- decky.logger.info(f" JSON file: {json_file}")
- decky.logger.info(f" lsfg script: {lsfg_script}")
-
- removed_files = []
-
- # Remove library file if it exists
- if os.path.exists(lib_file):
- decky.logger.info(f"Found library file, attempting to remove: {lib_file}")
- try:
- os.remove(lib_file)
- removed_files.append(lib_file)
- decky.logger.info(f"Successfully removed {lib_file}")
- except Exception as e:
- decky.logger.error(f"Failed to remove {lib_file}: {str(e)}")
- else:
- decky.logger.info(f"Library file not found: {lib_file}")
-
- # Remove JSON file if it exists
- if os.path.exists(json_file):
- decky.logger.info(f"Found JSON file, attempting to remove: {json_file}")
- try:
- os.remove(json_file)
- removed_files.append(json_file)
- decky.logger.info(f"Successfully removed {json_file}")
- except Exception as e:
- decky.logger.error(f"Failed to remove {json_file}: {str(e)}")
- else:
- decky.logger.info(f"JSON file not found: {json_file}")
-
- # Remove lsfg script if it exists
- if os.path.exists(lsfg_script):
- decky.logger.info(f"Found lsfg script, attempting to remove: {lsfg_script}")
- try:
- os.remove(lsfg_script)
- removed_files.append(lsfg_script)
- decky.logger.info(f"Successfully removed {lsfg_script}")
- except Exception as e:
- decky.logger.error(f"Failed to remove {lsfg_script}: {str(e)}")
- else:
- decky.logger.info(f"lsfg script not found: {lsfg_script}")
-
- if removed_files:
- decky.logger.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}")
- else:
- decky.logger.info("No lsfg-vk files found to clean up during plugin uninstall")
-
- except Exception as e:
- decky.logger.error(f"Error cleaning up lsfg-vk files during uninstall: {str(e)}")
- import traceback
- decky.logger.error(f"Traceback: {traceback.format_exc()}")
-
- decky.logger.info("Lossless Scaling uninstall cleanup completed")
- pass
+ """Function called after `_unload` during uninstall"""
+ await self.lifecycle_manager.on_uninstall()
- # Migrations that should be performed before entering `_main()`.
async def _migration(self):
- decky.logger.info("Migrating")
- # Here's a migration example for logs:
- # - `~/.config/decky-template/template.log` will be migrated to `decky.decky_LOG_DIR/template.log`
- decky.migrate_logs(os.path.join(decky.DECKY_USER_HOME,
- ".config", "decky-template", "template.log"))
- # Here's a migration example for settings:
- # - `~/homebrew/settings/template.json` is migrated to `decky.decky_SETTINGS_DIR/template.json`
- # - `~/.config/decky-template/` all files and directories under this root are migrated to `decky.decky_SETTINGS_DIR/`
- decky.migrate_settings(
- os.path.join(decky.DECKY_HOME, "settings", "template.json"),
- os.path.join(decky.DECKY_USER_HOME, ".config", "decky-template"))
- # Here's a migration example for runtime data:
- # - `~/homebrew/template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/`
- # - `~/.local/share/decky-template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/`
- decky.migrate_runtime(
- os.path.join(decky.DECKY_HOME, "template"),
- os.path.join(decky.DECKY_USER_HOME, ".local", "share", "decky-template"))
+ """Migrations that should be performed before entering `_main()`"""
+ await self.lifecycle_manager.on_migration()
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
new file mode 100644
index 0000000..a35f274
--- /dev/null
+++ b/src/api/lsfgApi.ts
@@ -0,0 +1,60 @@
+import { callable } from "@decky/api";
+
+// Type definitions for API responses
+export interface InstallationResult {
+ success: boolean;
+ error?: string;
+ message?: string;
+ removed_files?: string[];
+}
+
+export interface InstallationStatus {
+ installed: boolean;
+ lib_exists: boolean;
+ json_exists: boolean;
+ script_exists: boolean;
+ lib_path: string;
+ json_path: string;
+ script_path: string;
+ error?: string;
+}
+
+export interface DllDetectionResult {
+ detected: boolean;
+ path?: string;
+ source?: string;
+ message?: string;
+ error?: string;
+}
+
+export interface LsfgConfig {
+ enable_lsfg: boolean;
+ multiplier: number;
+ flow_scale: number;
+ hdr: boolean;
+ perf_mode: boolean;
+ immediate_mode: boolean;
+}
+
+export interface ConfigResult {
+ success: boolean;
+ config?: LsfgConfig;
+ error?: string;
+}
+
+export interface ConfigUpdateResult {
+ success: boolean;
+ message?: string;
+ error?: string;
+}
+
+// API functions
+export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk");
+export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk");
+export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg_vk_installed");
+export const checkLosslessScalingDll = callable<[], DllDetectionResult>("check_lossless_scaling_dll");
+export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config");
+export const updateLsfgConfig = callable<
+ [boolean, number, number, boolean, boolean, boolean],
+ ConfigUpdateResult
+>("update_lsfg_config");
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx
new file mode 100644
index 0000000..707ad2a
--- /dev/null
+++ b/src/components/ConfigurationSection.tsx
@@ -0,0 +1,115 @@
+import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui";
+
+interface LsfgConfig {
+ enableLsfg: boolean;
+ multiplier: number;
+ flowScale: number;
+ hdr: boolean;
+ perfMode: boolean;
+ immediateMode: boolean;
+}
+
+interface ConfigurationSectionProps {
+ config: LsfgConfig;
+ onEnableLsfgChange: (value: boolean) => Promise<void>;
+ onMultiplierChange: (value: number) => Promise<void>;
+ onFlowScaleChange: (value: number) => Promise<void>;
+ onHdrChange: (value: boolean) => Promise<void>;
+ onPerfModeChange: (value: boolean) => Promise<void>;
+ onImmediateModeChange: (value: boolean) => Promise<void>;
+}
+
+export function ConfigurationSection({
+ config,
+ onEnableLsfgChange,
+ onMultiplierChange,
+ onFlowScaleChange,
+ onHdrChange,
+ onPerfModeChange,
+ onImmediateModeChange
+}: ConfigurationSectionProps) {
+ return (
+ <>
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "14px",
+ fontWeight: "bold",
+ marginTop: "16px",
+ marginBottom: "8px",
+ borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
+ paddingBottom: "4px"
+ }}
+ >
+ LSFG Configuration
+ </div>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="Enable LSFG"
+ description="Enables the frame generation layer"
+ checked={config.enableLsfg}
+ onChange={onEnableLsfgChange}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <SliderField
+ label="FPS Multiplier"
+ description="Traditional FPS multiplier value"
+ value={config.multiplier}
+ min={2}
+ max={4}
+ step={1}
+ notchCount={3}
+ notchLabels={[
+ { notchIndex: 0, label: "2X" },
+ { notchIndex: 1, label: "3X" },
+ { notchIndex: 2, label: "4X" }
+ ]}
+ onChange={onMultiplierChange}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <SliderField
+ label={`Flow Scale ${Math.round(config.flowScale * 100)}%`}
+ description="Lowers the generated frame's resolution"
+ value={config.flowScale}
+ min={0.25}
+ max={1.0}
+ step={0.01}
+ onChange={onFlowScaleChange}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="HDR Mode"
+ description="Enable HDR mode (only if Game supports HDR)"
+ checked={config.hdr}
+ onChange={onHdrChange}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="Performance Mode"
+ description="Use lighter model for FG (experimental)"
+ checked={config.perfMode}
+ onChange={onPerfModeChange}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="Immediate Mode"
+ description="Disable vsync for reduced input lag"
+ checked={config.immediateMode}
+ onChange={onImmediateModeChange}
+ />
+ </PanelSectionRow>
+ </>
+ );
+}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
new file mode 100644
index 0000000..cecb142
--- /dev/null
+++ b/src/components/Content.tsx
@@ -0,0 +1,108 @@
+import { useEffect } from "react";
+import { PanelSection } from "@decky/ui";
+import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks";
+import { useInstallationActions } from "../hooks/useInstallationActions";
+import { StatusDisplay } from "./StatusDisplay";
+import { InstallationButton } from "./InstallationButton";
+import { ConfigurationSection } from "./ConfigurationSection";
+import { UsageInstructions } from "./UsageInstructions";
+
+export function Content() {
+ const {
+ isInstalled,
+ installationStatus,
+ setIsInstalled,
+ setInstallationStatus
+ } = useInstallationStatus();
+
+ const { dllDetected, dllDetectionStatus } = useDllDetection();
+
+ const {
+ config,
+ setters,
+ loadLsfgConfig,
+ updateConfig
+ } = useLsfgConfig();
+
+ const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions();
+
+ // Reload config when installation status changes
+ useEffect(() => {
+ if (isInstalled) {
+ loadLsfgConfig();
+ }
+ }, [isInstalled, loadLsfgConfig]);
+
+ // Configuration change handlers
+ const handleEnableLsfgChange = async (value: boolean) => {
+ setters.setEnableLsfg(value);
+ await updateConfig(value, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode);
+ };
+
+ const handleMultiplierChange = async (value: number) => {
+ setters.setMultiplier(value);
+ await updateConfig(config.enableLsfg, value, config.flowScale, config.hdr, config.perfMode, config.immediateMode);
+ };
+
+ const handleFlowScaleChange = async (value: number) => {
+ setters.setFlowScale(value);
+ await updateConfig(config.enableLsfg, config.multiplier, value, config.hdr, config.perfMode, config.immediateMode);
+ };
+
+ const handleHdrChange = async (value: boolean) => {
+ setters.setHdr(value);
+ await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, value, config.perfMode, config.immediateMode);
+ };
+
+ const handlePerfModeChange = async (value: boolean) => {
+ setters.setPerfMode(value);
+ await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, value, config.immediateMode);
+ };
+
+ const handleImmediateModeChange = async (value: boolean) => {
+ setters.setImmediateMode(value);
+ await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, value);
+ };
+
+ const onInstall = () => {
+ handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig);
+ };
+
+ const onUninstall = () => {
+ handleUninstall(setIsInstalled, setInstallationStatus);
+ };
+
+ return (
+ <PanelSection>
+ <StatusDisplay
+ dllDetected={dllDetected}
+ dllDetectionStatus={dllDetectionStatus}
+ isInstalled={isInstalled}
+ installationStatus={installationStatus}
+ />
+
+ <InstallationButton
+ isInstalled={isInstalled}
+ isInstalling={isInstalling}
+ isUninstalling={isUninstalling}
+ onInstall={onInstall}
+ onUninstall={onUninstall}
+ />
+
+ {/* Configuration Section - only show if installed */}
+ {isInstalled && (
+ <ConfigurationSection
+ config={config}
+ onEnableLsfgChange={handleEnableLsfgChange}
+ onMultiplierChange={handleMultiplierChange}
+ onFlowScaleChange={handleFlowScaleChange}
+ onHdrChange={handleHdrChange}
+ onPerfModeChange={handlePerfModeChange}
+ onImmediateModeChange={handleImmediateModeChange}
+ />
+ )}
+
+ <UsageInstructions multiplier={config.multiplier} />
+ </PanelSection>
+ );
+}
diff --git a/src/components/InstallationButton.tsx b/src/components/InstallationButton.tsx
new file mode 100644
index 0000000..7892678
--- /dev/null
+++ b/src/components/InstallationButton.tsx
@@ -0,0 +1,64 @@
+import { ButtonItem, PanelSectionRow } from "@decky/ui";
+import { FaDownload, FaTrash } from "react-icons/fa";
+
+interface InstallationButtonProps {
+ isInstalled: boolean;
+ isInstalling: boolean;
+ isUninstalling: boolean;
+ onInstall: () => void;
+ onUninstall: () => void;
+}
+
+export function InstallationButton({
+ isInstalled,
+ isInstalling,
+ isUninstalling,
+ onInstall,
+ onUninstall
+}: InstallationButtonProps) {
+ const renderButtonContent = () => {
+ if (isInstalling) {
+ return (
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+ <div>Installing...</div>
+ </div>
+ );
+ }
+
+ if (isUninstalling) {
+ return (
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+ <div>Uninstalling...</div>
+ </div>
+ );
+ }
+
+ if (isInstalled) {
+ return (
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+ <FaTrash />
+ <div>Uninstall lsfg-vk</div>
+ </div>
+ );
+ }
+
+ return (
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+ <FaDownload />
+ <div>Install lsfg-vk</div>
+ </div>
+ );
+ };
+
+ return (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={isInstalled ? onUninstall : onInstall}
+ disabled={isInstalling || isUninstalling}
+ >
+ {renderButtonContent()}
+ </ButtonItem>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx
new file mode 100644
index 0000000..8eecd42
--- /dev/null
+++ b/src/components/StatusDisplay.tsx
@@ -0,0 +1,38 @@
+import { PanelSectionRow } from "@decky/ui";
+
+interface StatusDisplayProps {
+ dllDetected: boolean;
+ dllDetectionStatus: string;
+ isInstalled: boolean;
+ installationStatus: string;
+}
+
+export function StatusDisplay({
+ dllDetected,
+ dllDetectionStatus,
+ isInstalled,
+ installationStatus
+}: StatusDisplayProps) {
+ return (
+ <PanelSectionRow>
+ <div style={{ marginBottom: "8px", fontSize: "14px" }}>
+ <div
+ style={{
+ color: dllDetected ? "#4CAF50" : "#F44336",
+ fontWeight: "bold",
+ marginBottom: "4px"
+ }}
+ >
+ {dllDetectionStatus}
+ </div>
+ <div
+ style={{
+ color: isInstalled ? "#4CAF50" : "#FF9800"
+ }}
+ >
+ Status: {installationStatus}
+ </div>
+ </div>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx
new file mode 100644
index 0000000..712d4c1
--- /dev/null
+++ b/src/components/UsageInstructions.tsx
@@ -0,0 +1,68 @@
+import { PanelSectionRow } from "@decky/ui";
+
+interface UsageInstructionsProps {
+ multiplier: number;
+}
+
+export function UsageInstructions({ multiplier }: UsageInstructionsProps) {
+ return (
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "13px",
+ marginTop: "12px",
+ padding: "8px",
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
+ borderRadius: "4px"
+ }}
+ >
+ <div style={{ fontWeight: "bold", marginBottom: "6px" }}>
+ Usage Instructions:
+ </div>
+ <div style={{ marginBottom: "4px" }}>
+ Option 1: Use the lsfg script (recommended):
+ </div>
+ <div
+ style={{
+ fontFamily: "monospace",
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
+ padding: "4px",
+ borderRadius: "2px",
+ fontSize: "12px",
+ marginBottom: "6px"
+ }}
+ >
+ ~/lsfg %COMMAND%
+ </div>
+ <div style={{ marginBottom: "4px" }}>
+ Option 2: Manual environment variables:
+ </div>
+ <div
+ style={{
+ fontFamily: "monospace",
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
+ padding: "4px",
+ borderRadius: "2px",
+ fontSize: "12px",
+ marginBottom: "6px"
+ }}
+ >
+ ENABLE_LSFG=1 LSFG_MULTIPLIER={multiplier} %COMMAND%
+ </div>
+ <div style={{ fontSize: "11px", opacity: 0.8 }}>
+ The lsfg script uses your current configuration settings.
+ <br />
+ • ENABLE_LSFG=1 - Enables frame generation
+ <br />
+ • LSFG_MULTIPLIER=2-4 - FPS multiplier (start with 2)
+ <br />
+ • LSFG_FLOW_SCALE=0.25-1.0 - Flow scale (for performance)
+ <br />
+ • LSFG_HDR=1 - HDR mode (only if using HDR)
+ <br />
+ • MESA_VK_WSI_PRESENT_MODE=immediate - Disable vsync
+ </div>
+ </div>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..77f11d4
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,5 @@
+export { Content } from "./Content";
+export { StatusDisplay } from "./StatusDisplay";
+export { InstallationButton } from "./InstallationButton";
+export { ConfigurationSection } from "./ConfigurationSection";
+export { UsageInstructions } from "./UsageInstructions";
diff --git a/src/hooks/useInstallationActions.ts b/src/hooks/useInstallationActions.ts
new file mode 100644
index 0000000..8dcf831
--- /dev/null
+++ b/src/hooks/useInstallationActions.ts
@@ -0,0 +1,89 @@
+import { useState } from "react";
+import { toaster } from "@decky/api";
+import { installLsfgVk, uninstallLsfgVk } from "../api/lsfgApi";
+
+export function useInstallationActions() {
+ const [isInstalling, setIsInstalling] = useState<boolean>(false);
+ const [isUninstalling, setIsUninstalling] = useState<boolean>(false);
+
+ const handleInstall = async (
+ setIsInstalled: (value: boolean) => void,
+ setInstallationStatus: (value: string) => void,
+ reloadConfig?: () => Promise<void>
+ ) => {
+ setIsInstalling(true);
+ setInstallationStatus("Installing lsfg-vk...");
+
+ try {
+ const result = await installLsfgVk();
+ if (result.success) {
+ setIsInstalled(true);
+ setInstallationStatus("lsfg-vk installed successfully!");
+ toaster.toast({
+ title: "Installation Complete",
+ body: "lsfg-vk has been installed successfully"
+ });
+
+ // Reload lsfg config after installation
+ if (reloadConfig) {
+ await reloadConfig();
+ }
+ } else {
+ setInstallationStatus(`Installation failed: ${result.error}`);
+ toaster.toast({
+ title: "Installation Failed",
+ body: result.error || "Unknown error occurred"
+ });
+ }
+ } catch (error) {
+ setInstallationStatus(`Installation failed: ${error}`);
+ toaster.toast({
+ title: "Installation Failed",
+ body: `Error: ${error}`
+ });
+ } finally {
+ setIsInstalling(false);
+ }
+ };
+
+ const handleUninstall = async (
+ setIsInstalled: (value: boolean) => void,
+ setInstallationStatus: (value: string) => void
+ ) => {
+ setIsUninstalling(true);
+ setInstallationStatus("Uninstalling lsfg-vk...");
+
+ try {
+ const result = await uninstallLsfgVk();
+ if (result.success) {
+ setIsInstalled(false);
+ setInstallationStatus("lsfg-vk uninstalled successfully!");
+ toaster.toast({
+ title: "Uninstallation Complete",
+ body: result.message || "lsfg-vk has been uninstalled successfully"
+ });
+ } else {
+ setInstallationStatus(`Uninstallation failed: ${result.error}`);
+ toaster.toast({
+ title: "Uninstallation Failed",
+ body: result.error || "Unknown error occurred"
+ });
+ }
+ } catch (error) {
+ setInstallationStatus(`Uninstallation failed: ${error}`);
+ toaster.toast({
+ title: "Uninstallation Failed",
+ body: `Error: ${error}`
+ });
+ } finally {
+ setIsUninstalling(false);
+ }
+ };
+
+ return {
+ isInstalling,
+ isUninstalling,
+ handleInstall,
+ handleUninstall
+ };
+}
diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts
new file mode 100644
index 0000000..76acd33
--- /dev/null
+++ b/src/hooks/useLsfgHooks.ts
@@ -0,0 +1,156 @@
+import { useState, useEffect } from "react";
+import { toaster } from "@decky/api";
+import {
+ checkLsfgVkInstalled,
+ checkLosslessScalingDll,
+ getLsfgConfig,
+ updateLsfgConfig,
+ type ConfigUpdateResult
+} from "../api/lsfgApi";
+
+export function useInstallationStatus() {
+ const [isInstalled, setIsInstalled] = useState<boolean>(false);
+ const [installationStatus, setInstallationStatus] = useState<string>("");
+
+ const checkInstallation = async () => {
+ try {
+ const status = await checkLsfgVkInstalled();
+ setIsInstalled(status.installed);
+ if (status.installed) {
+ setInstallationStatus("lsfg-vk is installed");
+ } else {
+ setInstallationStatus("lsfg-vk is not installed");
+ }
+ return status.installed;
+ } catch (error) {
+ setInstallationStatus("Error checking installation status");
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ checkInstallation();
+ }, []);
+
+ return {
+ isInstalled,
+ installationStatus,
+ setIsInstalled,
+ setInstallationStatus,
+ checkInstallation
+ };
+}
+
+export function useDllDetection() {
+ const [dllDetected, setDllDetected] = useState<boolean>(false);
+ const [dllDetectionStatus, setDllDetectionStatus] = useState<string>("");
+
+ const checkDllDetection = async () => {
+ try {
+ const result = await checkLosslessScalingDll();
+ setDllDetected(result.detected);
+ if (result.detected) {
+ setDllDetectionStatus(`Lossless Scaling App detected (${result.source})`);
+ } else {
+ setDllDetectionStatus(result.message || "Lossless Scaling App not detected");
+ }
+ } catch (error) {
+ setDllDetectionStatus("Error checking Lossless Scaling App");
+ }
+ };
+
+ useEffect(() => {
+ checkDllDetection();
+ }, []);
+
+ return {
+ dllDetected,
+ dllDetectionStatus
+ };
+}
+
+export function useLsfgConfig() {
+ const [enableLsfg, setEnableLsfg] = useState<boolean>(true);
+ const [multiplier, setMultiplier] = useState<number>(2);
+ const [flowScale, setFlowScale] = useState<number>(1.0);
+ const [hdr, setHdr] = useState<boolean>(false);
+ const [perfMode, setPerfMode] = useState<boolean>(false);
+ const [immediateMode, setImmediateMode] = useState<boolean>(false);
+
+ const loadLsfgConfig = async () => {
+ try {
+ const result = await getLsfgConfig();
+ if (result.success && result.config) {
+ setEnableLsfg(result.config.enable_lsfg);
+ setMultiplier(result.config.multiplier);
+ setFlowScale(result.config.flow_scale);
+ setHdr(result.config.hdr);
+ setPerfMode(result.config.perf_mode);
+ setImmediateMode(result.config.immediate_mode);
+ console.log("Loaded lsfg config:", result.config);
+ } else {
+ console.log("lsfg config not available, using defaults:", result.error);
+ }
+ } catch (error) {
+ console.error("Error loading lsfg config:", error);
+ }
+ };
+
+ const updateConfig = async (
+ newEnableLsfg: boolean,
+ newMultiplier: number,
+ newFlowScale: number,
+ newHdr: boolean,
+ newPerfMode: boolean,
+ newImmediateMode: boolean
+ ): Promise<ConfigUpdateResult> => {
+ try {
+ const result = await updateLsfgConfig(
+ newEnableLsfg,
+ newMultiplier,
+ newFlowScale,
+ newHdr,
+ newPerfMode,
+ newImmediateMode
+ );
+ if (!result.success) {
+ toaster.toast({
+ title: "Update Failed",
+ body: result.error || "Failed to update configuration"
+ });
+ }
+ return result;
+ } catch (error) {
+ toaster.toast({
+ title: "Update Failed",
+ body: `Error: ${error}`
+ });
+ return { success: false, error: String(error) };
+ }
+ };
+
+ useEffect(() => {
+ loadLsfgConfig();
+ }, []);
+
+ return {
+ config: {
+ enableLsfg,
+ multiplier,
+ flowScale,
+ hdr,
+ perfMode,
+ immediateMode
+ },
+ setters: {
+ setEnableLsfg,
+ setMultiplier,
+ setFlowScale,
+ setHdr,
+ setPerfMode,
+ setImmediateMode
+ },
+ loadLsfgConfig,
+ updateConfig
+ };
+}
diff --git a/src/index.tsx b/src/index.tsx
index 2b895b3..e8ab56f 100755
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,448 +1,10 @@
-import {
- ButtonItem,
- PanelSection,
- PanelSectionRow,
- staticClasses,
- ToggleField,
- SliderField
-} from "@decky/ui";
-import {
- callable,
- definePlugin,
- toaster,
-} from "@decky/api"
-import { useState, useEffect } from "react";
-import { FaDownload, FaTrash } from "react-icons/fa";
+import { staticClasses } from "@decky/ui";
+import { definePlugin } from "@decky/api";
import { GiPlasticDuck } from "react-icons/gi";
-
-// Function to install lsfg-vk
-const installLsfgVk = callable<[], { success: boolean; error?: string; message?: string }>("install_lsfg_vk");
-
-// Function to uninstall lsfg-vk
-const uninstallLsfgVk = callable<[], { success: boolean; error?: string; message?: string; removed_files?: string[] }>("uninstall_lsfg_vk");
-
-// Function to check if lsfg-vk is installed
-const checkLsfgVkInstalled = callable<[], { installed: boolean; lib_exists: boolean; json_exists: boolean; script_exists: boolean; lib_path: string; json_path: string; script_path: string; error?: string }>("check_lsfg_vk_installed");
-
-// Function to check if Lossless Scaling DLL is available
-const checkLosslessScalingDll = callable<[], { detected: boolean; path?: string; source?: string; message?: string; error?: string }>("check_lossless_scaling_dll");
-
-// Function to get lsfg configuration
-const getLsfgConfig = callable<[], { success: boolean; config?: { enable_lsfg: boolean; multiplier: number; flow_scale: number; hdr: boolean; perf_mode: boolean; immediate_mode: boolean }; error?: string }>("get_lsfg_config");
-
-// Function to update lsfg configuration
-const updateLsfgConfig = callable<[boolean, number, number, boolean, boolean, boolean], { success: boolean; message?: string; error?: string }>("update_lsfg_config");
-
-function Content() {
- const [isInstalled, setIsInstalled] = useState<boolean>(false);
- const [isInstalling, setIsInstalling] = useState<boolean>(false);
- const [isUninstalling, setIsUninstalling] = useState<boolean>(false);
- const [installationStatus, setInstallationStatus] = useState<string>("");
- const [dllDetected, setDllDetected] = useState<boolean>(false);
- const [dllDetectionStatus, setDllDetectionStatus] = useState<string>("");
-
- // LSFG configuration state
- const [enableLsfg, setEnableLsfg] = useState<boolean>(true);
- const [multiplier, setMultiplier] = useState<number>(2);
- const [flowScale, setFlowScale] = useState<number>(1.0);
- const [hdr, setHdr] = useState<boolean>(false);
- const [perfMode, setPerfMode] = useState<boolean>(false);
- const [immediateMode, setImmediateMode] = useState<boolean>(false);
-
- // Check installation status on component mount
- useEffect(() => {
- const checkInstallation = async () => {
- try {
- const status = await checkLsfgVkInstalled();
- setIsInstalled(status.installed);
- if (status.installed) {
- setInstallationStatus("lsfg-vk is installed");
- } else {
- setInstallationStatus("lsfg-vk is not installed");
- }
- } catch (error) {
- setInstallationStatus("Error checking installation status");
- }
- };
-
- const checkDllDetection = async () => {
- try {
- const result = await checkLosslessScalingDll();
- setDllDetected(result.detected);
- if (result.detected) {
- setDllDetectionStatus(`Lossless Scaling App detected (${result.source})`);
- } else {
- setDllDetectionStatus(result.message || "Lossless Scaling App not detected");
- }
- } catch (error) {
- setDllDetectionStatus("Error checking Lossless Scaling App");
- }
- };
-
- const loadLsfgConfig = async () => {
- try {
- const result = await getLsfgConfig();
- if (result.success && result.config) {
- setEnableLsfg(result.config.enable_lsfg);
- setMultiplier(result.config.multiplier);
- setFlowScale(result.config.flow_scale);
- setHdr(result.config.hdr);
- setPerfMode(result.config.perf_mode);
- setImmediateMode(result.config.immediate_mode);
- console.log("Loaded lsfg config:", result.config);
- } else {
- // If script doesn't exist or can't be read, keep default values
- console.log("lsfg config not available, using defaults:", result.error);
- }
- } catch (error) {
- console.error("Error loading lsfg config:", error);
- }
- };
-
- checkInstallation();
- checkDllDetection();
-
- // Always try to load config, regardless of installation status
- // This handles cases where the script exists but plugin shows as not installed
- loadLsfgConfig();
- }, []);
-
- // Add a second useEffect to reload config when isInstalled changes
- // This ensures UI reflects script state when plugin detects installation
- useEffect(() => {
- if (isInstalled) {
- const reloadConfig = async () => {
- try {
- const result = await getLsfgConfig();
- if (result.success && result.config) {
- setEnableLsfg(result.config.enable_lsfg);
- setMultiplier(result.config.multiplier);
- setFlowScale(result.config.flow_scale);
- setHdr(result.config.hdr);
- setPerfMode(result.config.perf_mode);
- setImmediateMode(result.config.immediate_mode);
- console.log("Reloaded lsfg config after installation detected:", result.config);
- }
- } catch (error) {
- console.error("Error reloading lsfg config:", error);
- }
- };
- reloadConfig();
- }
- }, [isInstalled]); const handleInstall = async () => {
- setIsInstalling(true);
- setInstallationStatus("Installing lsfg-vk...");
-
- try {
- const result = await installLsfgVk();
- if (result.success) {
- setIsInstalled(true);
- setInstallationStatus("lsfg-vk installed successfully!");
- toaster.toast({
- title: "Installation Complete",
- body: "lsfg-vk has been installed successfully"
- });
-
- // Reload lsfg config after installation
- try {
- const configResult = await getLsfgConfig();
- if (configResult.success && configResult.config) {
- setEnableLsfg(configResult.config.enable_lsfg);
- setMultiplier(configResult.config.multiplier);
- setFlowScale(configResult.config.flow_scale);
- setHdr(configResult.config.hdr);
- setPerfMode(configResult.config.perf_mode);
- setImmediateMode(configResult.config.immediate_mode);
- }
- } catch (error) {
- console.error("Error reloading config after install:", error);
- }
- } else {
- setInstallationStatus(`Installation failed: ${result.error}`);
- toaster.toast({
- title: "Installation Failed",
- body: result.error || "Unknown error occurred"
- });
- }
- } catch (error) {
- setInstallationStatus(`Installation failed: ${error}`);
- toaster.toast({
- title: "Installation Failed",
- body: `Error: ${error}`
- });
- } finally {
- setIsInstalling(false);
- }
- };
-
- const handleUninstall = async () => {
- setIsUninstalling(true);
- setInstallationStatus("Uninstalling lsfg-vk...");
-
- try {
- const result = await uninstallLsfgVk();
- if (result.success) {
- setIsInstalled(false);
- setInstallationStatus("lsfg-vk uninstalled successfully!");
- toaster.toast({
- title: "Uninstallation Complete",
- body: result.message || "lsfg-vk has been uninstalled successfully"
- });
- } else {
- setInstallationStatus(`Uninstallation failed: ${result.error}`);
- toaster.toast({
- title: "Uninstallation Failed",
- body: result.error || "Unknown error occurred"
- });
- }
- } catch (error) {
- setInstallationStatus(`Uninstallation failed: ${error}`);
- toaster.toast({
- title: "Uninstallation Failed",
- body: `Error: ${error}`
- });
- } finally {
- setIsUninstalling(false);
- }
- };
-
- const updateConfig = async (newEnableLsfg: boolean, newMultiplier: number, newFlowScale: number, newHdr: boolean, newPerfMode: boolean, newImmediateMode: boolean) => {
- try {
- const result = await updateLsfgConfig(newEnableLsfg, newMultiplier, newFlowScale, newHdr, newPerfMode, newImmediateMode);
- if (!result.success) {
- toaster.toast({
- title: "Update Failed",
- body: result.error || "Failed to update configuration"
- });
- }
- // Only show error notifications, not success notifications to avoid spam
- } catch (error) {
- toaster.toast({
- title: "Update Failed",
- body: `Error: ${error}`
- });
- }
- };
-
- const handleEnableLsfgChange = async (value: boolean) => {
- setEnableLsfg(value);
- await updateConfig(value, multiplier, flowScale, hdr, perfMode, immediateMode);
- };
-
- const handleMultiplierChange = async (value: number) => {
- setMultiplier(value);
- await updateConfig(enableLsfg, value, flowScale, hdr, perfMode, immediateMode);
- };
-
- const handleFlowScaleChange = async (value: number) => {
- setFlowScale(value);
- await updateConfig(enableLsfg, multiplier, value, hdr, perfMode, immediateMode);
- };
-
- const handleHdrChange = async (value: boolean) => {
- setHdr(value);
- await updateConfig(enableLsfg, multiplier, flowScale, value, perfMode, immediateMode);
- };
-
- const handlePerfModeChange = async (value: boolean) => {
- setPerfMode(value);
- await updateConfig(enableLsfg, multiplier, flowScale, hdr, value, immediateMode);
- };
-
- const handleImmediateModeChange = async (value: boolean) => {
- setImmediateMode(value);
- await updateConfig(enableLsfg, multiplier, flowScale, hdr, perfMode, value);
- };
-
- return (
- <PanelSection>
- <PanelSectionRow>
- <div style={{ marginBottom: "8px", fontSize: "14px" }}>
- <div style={{
- color: dllDetected ? "#4CAF50" : "#F44336",
- fontWeight: "bold",
- marginBottom: "4px"
- }}>
- {dllDetectionStatus}
- </div>
- <div style={{
- color: isInstalled ? "#4CAF50" : "#FF9800"
- }}>
- Status: {installationStatus}
- </div>
- </div>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={isInstalled ? handleUninstall : handleInstall}
- disabled={isInstalling || isUninstalling}
- >
- {isInstalling ? (
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <div>Installing...</div>
- </div>
- ) : isUninstalling ? (
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <div>Uninstalling...</div>
- </div>
- ) : isInstalled ? (
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaTrash />
- <div>Uninstall lsfg-vk</div>
- </div>
- ) : (
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaDownload />
- <div>Install lsfg-vk</div>
- </div>
- )}
- </ButtonItem>
- </PanelSectionRow>
-
- {/* Configuration Section - only show if installed */}
- {isInstalled && (
- <>
- <PanelSectionRow>
- <div style={{
- fontSize: "14px",
- fontWeight: "bold",
- marginTop: "16px",
- marginBottom: "8px",
- borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
- paddingBottom: "4px"
- }}>
- LSFG Configuration
- </div>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ToggleField
- label="Enable LSFG"
- description="Enables the frame generation layer"
- checked={enableLsfg}
- onChange={handleEnableLsfgChange}
- />
- </PanelSectionRow>
-
- <PanelSectionRow>
- <SliderField
- label="FPS Multiplier"
- description="Traditional FPS multiplier value (2-4)"
- value={multiplier}
- min={2}
- max={4}
- step={1}
- notchCount={3}
- notchLabels={[
- { notchIndex: 0, label: "2" },
- { notchIndex: 1, label: "3" },
- { notchIndex: 2, label: "4" }
- ]}
- onChange={handleMultiplierChange}
- />
- </PanelSectionRow>
-
- <PanelSectionRow>
- <SliderField
- label={`Flow Scale ${Math.round(flowScale * 100)}%`}
- description="Lowers the flow scale for performance (0.25-1.0)"
- value={flowScale}
- min={0.25}
- max={1.0}
- step={0.01}
- onChange={handleFlowScaleChange}
- />
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ToggleField
- label="HDR Mode"
- description="Enable HDR mode (only if using HDR)"
- checked={hdr}
- onChange={handleHdrChange}
- />
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ToggleField
- label="Performance Mode"
- description="Enable performance mode (can quintuple performance)"
- checked={perfMode}
- onChange={handlePerfModeChange}
- />
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ToggleField
- label="Immediate Mode"
- description="Disable vsync for reduced input lag"
- checked={immediateMode}
- onChange={handleImmediateModeChange}
- />
- </PanelSectionRow>
- </>
- )}
-
- <PanelSectionRow>
- <div style={{
- fontSize: "13px",
- marginTop: "12px",
- padding: "8px",
- backgroundColor: "rgba(255, 255, 255, 0.05)",
- borderRadius: "4px"
- }}>
- <div style={{ fontWeight: "bold", marginBottom: "6px" }}>
- Usage Instructions:
- </div>
- <div style={{ marginBottom: "4px" }}>
- Option 1: Use the lsfg script (recommended):
- </div>
- <div style={{
- fontFamily: "monospace",
- backgroundColor: "rgba(0, 0, 0, 0.3)",
- padding: "4px",
- borderRadius: "2px",
- fontSize: "12px",
- marginBottom: "6px"
- }}>
- ~/lsfg %COMMAND%
- </div>
- <div style={{ marginBottom: "4px" }}>
- Option 2: Manual environment variables:
- </div>
- <div style={{
- fontFamily: "monospace",
- backgroundColor: "rgba(0, 0, 0, 0.3)",
- padding: "4px",
- borderRadius: "2px",
- fontSize: "12px",
- marginBottom: "6px"
- }}>
- ENABLE_LSFG=1 LSFG_MULTIPLIER={multiplier} %COMMAND%
- </div>
- <div style={{ fontSize: "11px", opacity: 0.8 }}>
- The lsfg script uses your current configuration settings.
- <br />
- • ENABLE_LSFG=1 - Enables frame generation
- <br />
- • LSFG_MULTIPLIER=2-4 - FPS multiplier (start with 2)
- <br />
- • LSFG_FLOW_SCALE=0.25-1.0 - Flow scale (for performance)
- <br />
- • LSFG_HDR=1 - HDR mode (only if using HDR)
- <br />
- • MESA_VK_WSI_PRESENT_MODE=immediate - Disable vsync
- </div>
- </div>
- </PanelSectionRow>
- </PanelSection>
- );
-};
+import { Content } from "./components";
export default definePlugin(() => {
- console.log("Lossless Scaling plugin initializing")
+ console.log("Lossless Scaling plugin initializing");
return {
// The name shown in various decky menus
@@ -455,7 +17,7 @@ export default definePlugin(() => {
icon: <GiPlasticDuck />,
// The function triggered when your plugin unloads
onDismount() {
- console.log("Lossless Scaling unloading")
- },
+ console.log("Lossless Scaling unloading");
+ }
};
});
diff --git a/src/plugin_lifecycle.py b/src/plugin_lifecycle.py
new file mode 100644
index 0000000..4fb5c69
--- /dev/null
+++ b/src/plugin_lifecycle.py
@@ -0,0 +1,47 @@
+import os
+import decky
+from .services import InstallationService
+
+
+class PluginLifecycleManager:
+ """Manages plugin lifecycle events"""
+
+ def __init__(self):
+ self.installation_service = InstallationService()
+
+ async def on_load(self):
+ """Called when plugin is loaded"""
+ decky.logger.info("Lossless Scaling loaded!")
+
+ async def on_unload(self):
+ """Called when plugin is unloaded"""
+ decky.logger.info("Lossless Scaling unloading")
+
+ async def on_uninstall(self):
+ """Called when plugin is uninstalled"""
+ decky.logger.info("Lossless Scaling uninstalled - starting cleanup")
+
+ # Clean up lsfg-vk files when the plugin is uninstalled
+ self.installation_service.cleanup_on_uninstall()
+
+ decky.logger.info("Lossless Scaling uninstall cleanup completed")
+
+ async def on_migration(self):
+ """Called during plugin migration"""
+ decky.logger.info("Migrating")
+ # Here's a migration example for logs:
+ # - `~/.config/decky-template/template.log` will be migrated to `decky.decky_LOG_DIR/template.log`
+ decky.migrate_logs(os.path.join(decky.DECKY_USER_HOME,
+ ".config", "decky-template", "template.log"))
+ # Here's a migration example for settings:
+ # - `~/homebrew/settings/template.json` is migrated to `decky.decky_SETTINGS_DIR/template.json`
+ # - `~/.config/decky-template/` all files and directories under this root are migrated to `decky.decky_SETTINGS_DIR/`
+ decky.migrate_settings(
+ os.path.join(decky.DECKY_HOME, "settings", "template.json"),
+ os.path.join(decky.DECKY_USER_HOME, ".config", "decky-template"))
+ # Here's a migration example for runtime data:
+ # - `~/homebrew/template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/`
+ # - `~/.local/share/decky-template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/`
+ decky.migrate_runtime(
+ os.path.join(decky.DECKY_HOME, "template"),
+ os.path.join(decky.DECKY_USER_HOME, ".local", "share", "decky-template"))
diff --git a/src/services/__init__.py b/src/services/__init__.py
new file mode 100644
index 0000000..a00af5c
--- /dev/null
+++ b/src/services/__init__.py
@@ -0,0 +1,5 @@
+from .installation_service import InstallationService
+from .dll_detection_service import DllDetectionService
+from .configuration_service import ConfigurationService
+
+__all__ = ["InstallationService", "DllDetectionService", "ConfigurationService"]
diff --git a/src/services/configuration_service.py b/src/services/configuration_service.py
new file mode 100644
index 0000000..a4975c5
--- /dev/null
+++ b/src/services/configuration_service.py
@@ -0,0 +1,164 @@
+import os
+from typing import Dict, Any
+import decky
+
+
+class ConfigurationService:
+ """Service for managing lsfg script configuration"""
+
+ def __init__(self):
+ self.user_home = os.path.expanduser("~")
+ self.lsfg_script_path = os.path.join(self.user_home, "lsfg")
+
+ async def get_config(self) -> Dict[str, Any]:
+ """Read current lsfg script configuration"""
+ try:
+ if not os.path.exists(self.lsfg_script_path):
+ return {
+ "success": False,
+ "error": "lsfg script not found"
+ }
+
+ with open(self.lsfg_script_path, 'r') as f:
+ content = f.read()
+
+ # Parse the script content to extract current values
+ config = {
+ "enable_lsfg": False,
+ "multiplier": 2,
+ "flow_scale": 1.0,
+ "hdr": False,
+ "perf_mode": False,
+ "immediate_mode": False
+ }
+
+ lines = content.split('\n')
+ for line in lines:
+ line = line.strip()
+
+ # Handle ENABLE_LSFG - check if it's commented out or not
+ if line.startswith('export ENABLE_LSFG='):
+ try:
+ value = line.split('=')[1].strip()
+ config["enable_lsfg"] = value == '1'
+ except:
+ pass
+ elif line.startswith('# export ENABLE_LSFG='):
+ config["enable_lsfg"] = False
+
+ # Handle LSFG_MULTIPLIER
+ elif line.startswith('export LSFG_MULTIPLIER='):
+ try:
+ value = line.split('=')[1].strip()
+ config["multiplier"] = int(value)
+ except:
+ pass
+
+ # Handle LSFG_FLOW_SCALE
+ elif line.startswith('export LSFG_FLOW_SCALE='):
+ try:
+ value = line.split('=')[1].strip()
+ config["flow_scale"] = float(value)
+ except:
+ pass
+
+ # Handle LSFG_HDR - check if it's commented out or not
+ elif line.startswith('export LSFG_HDR='):
+ try:
+ value = line.split('=')[1].strip()
+ config["hdr"] = value == '1'
+ except:
+ pass
+ elif line.startswith('# export LSFG_HDR='):
+ config["hdr"] = False
+
+ # Handle LSFG_PERF_MODE - check if it's commented out or not
+ elif line.startswith('export LSFG_PERF_MODE='):
+ try:
+ value = line.split('=')[1].strip()
+ config["perf_mode"] = value == '1'
+ except:
+ pass
+ elif line.startswith('# export LSFG_PERF_MODE='):
+ config["perf_mode"] = False
+
+ # Handle MESA_VK_WSI_PRESENT_MODE - check if it's commented out or not
+ elif line.startswith('export MESA_VK_WSI_PRESENT_MODE='):
+ try:
+ value = line.split('=')[1].strip()
+ # Remove any comments after the value
+ value = value.split('#')[0].strip()
+ config["immediate_mode"] = value == 'immediate'
+ except:
+ pass
+ elif line.startswith('# export MESA_VK_WSI_PRESENT_MODE='):
+ config["immediate_mode"] = False
+
+ decky.logger.info(f"Parsed lsfg config: {config}")
+
+ return {
+ "success": True,
+ "config": config
+ }
+
+ except Exception as e:
+ decky.logger.error(f"Error reading lsfg config: {str(e)}")
+ return {
+ "success": False,
+ "error": str(e)
+ }
+
+ async def update_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float,
+ hdr: bool, perf_mode: bool, immediate_mode: bool) -> Dict[str, Any]:
+ """Update lsfg script configuration"""
+ try:
+ # Create script content based on parameters
+ script_content = "#!/bin/bash\n\n"
+
+ if enable_lsfg:
+ script_content += "export ENABLE_LSFG=1\n"
+ else:
+ script_content += "# export ENABLE_LSFG=1\n"
+
+ script_content += f"export LSFG_MULTIPLIER={multiplier}\n"
+ script_content += f"export LSFG_FLOW_SCALE={flow_scale}\n"
+
+ if hdr:
+ script_content += "export LSFG_HDR=1\n"
+ else:
+ script_content += "# export LSFG_HDR=1\n"
+
+ if perf_mode:
+ script_content += "export LSFG_PERF_MODE=1\n"
+ else:
+ script_content += "# export LSFG_PERF_MODE=1\n"
+
+ if immediate_mode:
+ script_content += "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n"
+ else:
+ script_content += "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n"
+
+ # Add the exec line to allow the script to execute passed commands
+ script_content += "\n# Execute the passed command with the environment variables set\n"
+ script_content += "exec \"$@\"\n"
+
+ # Write the updated script
+ with open(self.lsfg_script_path, 'w') as f:
+ f.write(script_content)
+
+ # Make sure it's executable
+ os.chmod(self.lsfg_script_path, 0o755)
+
+ decky.logger.info(f"Updated lsfg script configuration: enable={enable_lsfg}, multiplier={multiplier}, flow_scale={flow_scale}, hdr={hdr}, perf_mode={perf_mode}, immediate_mode={immediate_mode}")
+
+ return {
+ "success": True,
+ "message": "lsfg configuration updated successfully"
+ }
+
+ except Exception as e:
+ decky.logger.error(f"Error updating lsfg config: {str(e)}")
+ return {
+ "success": False,
+ "error": str(e)
+ }
diff --git a/src/services/dll_detection_service.py b/src/services/dll_detection_service.py
new file mode 100644
index 0000000..095b3b5
--- /dev/null
+++ b/src/services/dll_detection_service.py
@@ -0,0 +1,60 @@
+import os
+from typing import Dict, Any
+import decky
+
+
+class DllDetectionService:
+ """Service for detecting Lossless Scaling DLL"""
+
+ async def check_lossless_scaling_dll(self) -> Dict[str, Any]:
+ """Check if Lossless Scaling DLL is available at the expected paths"""
+ try:
+ # Check environment variable first
+ dll_path = os.getenv("LSFG_DLL_PATH")
+ if dll_path and dll_path.strip():
+ dll_path_str = dll_path.strip()
+ if os.path.exists(dll_path_str):
+ return {
+ "detected": True,
+ "path": dll_path_str,
+ "source": "LSFG_DLL_PATH environment variable"
+ }
+
+ # Check XDG_DATA_HOME path
+ data_dir = os.getenv("XDG_DATA_HOME")
+ if data_dir and data_dir.strip():
+ dll_path_str = os.path.join(data_dir.strip(), "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll")
+ if os.path.exists(dll_path_str):
+ return {
+ "detected": True,
+ "path": dll_path_str,
+ "source": "XDG_DATA_HOME Steam directory"
+ }
+
+ # Check HOME/.local/share path
+ home_dir = os.getenv("HOME")
+ if home_dir and home_dir.strip():
+ dll_path_str = os.path.join(home_dir.strip(), ".local", "share", "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll")
+ if os.path.exists(dll_path_str):
+ return {
+ "detected": True,
+ "path": dll_path_str,
+ "source": "HOME/.local/share Steam directory"
+ }
+
+ # DLL not found in any expected location
+ return {
+ "detected": False,
+ "path": None,
+ "source": None,
+ "message": "Lossless Scaling DLL not found in expected locations"
+ }
+
+ except Exception as e:
+ decky.logger.error(f"Error checking Lossless Scaling DLL: {str(e)}")
+ return {
+ "detected": False,
+ "path": None,
+ "source": None,
+ "error": str(e)
+ }
diff --git a/src/services/installation_service.py b/src/services/installation_service.py
new file mode 100644
index 0000000..92d5132
--- /dev/null
+++ b/src/services/installation_service.py
@@ -0,0 +1,205 @@
+import os
+import zipfile
+import shutil
+import tempfile
+from typing import Dict, Any
+import decky
+
+
+class InstallationService:
+ """Service for handling lsfg-vk installation and uninstallation"""
+
+ def __init__(self):
+ self.user_home = os.path.expanduser("~")
+ self.local_lib_dir = os.path.join(self.user_home, ".local", "lib")
+ self.local_share_dir = os.path.join(self.user_home, ".local", "share", "vulkan", "implicit_layer.d")
+ self.lsfg_script_path = os.path.join(self.user_home, "lsfg")
+
+ # File paths
+ self.lib_file = os.path.join(self.local_lib_dir, "liblsfg-vk.so")
+ self.json_file = os.path.join(self.local_share_dir, "VkLayer_LS_frame_generation.json")
+
+ async def install(self) -> Dict[str, Any]:
+ """Install lsfg-vk by extracting the zip file to ~/.local"""
+ try:
+ # Get the path to the lsfg-vk_archlinux.zip file in the bin directory
+ plugin_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+ zip_path = os.path.join(plugin_dir, "bin", "lsfg-vk_archlinux.zip")
+
+ # Check if the zip file exists
+ if not os.path.exists(zip_path):
+ decky.logger.error(f"lsfg-vk_archlinux.zip not found at {zip_path}")
+ return {"success": False, "error": "lsfg-vk_archlinux.zip file not found"}
+
+ # Create directories if they don't exist
+ os.makedirs(self.local_lib_dir, exist_ok=True)
+ os.makedirs(self.local_share_dir, exist_ok=True)
+
+ # Extract the zip file
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ # Use /tmp for temporary extraction since we may not have write permissions in plugin dir
+ with tempfile.TemporaryDirectory() as temp_dir:
+ zip_ref.extractall(temp_dir)
+
+ # Look for the extracted files and copy them to the correct locations
+ for root, dirs, files in os.walk(temp_dir):
+ for file in files:
+ src_file = os.path.join(root, file)
+ if file.endswith('.so'):
+ # Copy library files to ~/.local/lib
+ dst_file = os.path.join(self.local_lib_dir, file)
+ shutil.copy2(src_file, dst_file)
+ decky.logger.info(f"Copied {file} to {dst_file}")
+ elif file.endswith('.json'):
+ # Copy JSON files to ~/.local/share/vulkan/implicit_layer.d
+ dst_file = os.path.join(self.local_share_dir, file)
+ shutil.copy2(src_file, dst_file)
+ decky.logger.info(f"Copied {file} to {dst_file}")
+
+ # Create the lsfg script
+ self._create_lsfg_script()
+
+ decky.logger.info("lsfg-vk installed successfully")
+ return {"success": True, "message": "lsfg-vk installed successfully"}
+
+ except Exception as e:
+ decky.logger.error(f"Error installing lsfg-vk: {str(e)}")
+ return {"success": False, "error": str(e)}
+
+ def _create_lsfg_script(self):
+ """Create the lsfg script in home directory"""
+ script_content = """#!/bin/bash
+
+export ENABLE_LSFG=1
+export LSFG_MULTIPLIER=2
+export LSFG_FLOW_SCALE=1.0
+# export LSFG_HDR=1
+# export LSFG_PERF_MODE=1
+# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync
+
+# Execute the passed command with the environment variables set
+exec "$@"
+"""
+
+ with open(self.lsfg_script_path, 'w') as script_file:
+ script_file.write(script_content)
+
+ # Make the script executable
+ os.chmod(self.lsfg_script_path, 0o755)
+ decky.logger.info(f"Created executable lsfg script at {self.lsfg_script_path}")
+
+ async def check_installation(self) -> Dict[str, Any]:
+ """Check if lsfg-vk is already installed"""
+ try:
+ lib_exists = os.path.exists(self.lib_file)
+ json_exists = os.path.exists(self.json_file)
+ script_exists = os.path.exists(self.lsfg_script_path)
+
+ decky.logger.info(f"Installation check: lib={lib_exists}, json={json_exists}, script={script_exists}")
+
+ return {
+ "installed": lib_exists and json_exists,
+ "lib_exists": lib_exists,
+ "json_exists": json_exists,
+ "script_exists": script_exists,
+ "lib_path": self.lib_file,
+ "json_path": self.json_file,
+ "script_path": self.lsfg_script_path
+ }
+
+ except Exception as e:
+ decky.logger.error(f"Error checking lsfg-vk installation: {str(e)}")
+ return {"installed": False, "error": str(e)}
+
+ async def uninstall(self) -> Dict[str, Any]:
+ """Uninstall lsfg-vk by removing the installed files"""
+ try:
+ removed_files = []
+
+ # Remove library file if it exists
+ if os.path.exists(self.lib_file):
+ os.remove(self.lib_file)
+ removed_files.append(self.lib_file)
+ decky.logger.info(f"Removed {self.lib_file}")
+
+ # Remove JSON file if it exists
+ if os.path.exists(self.json_file):
+ os.remove(self.json_file)
+ removed_files.append(self.json_file)
+ decky.logger.info(f"Removed {self.json_file}")
+
+ # Remove lsfg script if it exists
+ if os.path.exists(self.lsfg_script_path):
+ os.remove(self.lsfg_script_path)
+ removed_files.append(self.lsfg_script_path)
+ decky.logger.info(f"Removed {self.lsfg_script_path}")
+
+ if not removed_files:
+ return {"success": True, "message": "No lsfg-vk files found to remove"}
+
+ decky.logger.info("lsfg-vk uninstalled successfully")
+ return {
+ "success": True,
+ "message": f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.",
+ "removed_files": removed_files
+ }
+
+ except Exception as e:
+ decky.logger.error(f"Error uninstalling lsfg-vk: {str(e)}")
+ return {"success": False, "error": str(e)}
+
+ def cleanup_on_uninstall(self) -> None:
+ """Clean up lsfg-vk files when the plugin is uninstalled"""
+ try:
+ decky.logger.info(f"Checking for lsfg-vk files to clean up:")
+ decky.logger.info(f" Library file: {self.lib_file}")
+ decky.logger.info(f" JSON file: {self.json_file}")
+ decky.logger.info(f" lsfg script: {self.lsfg_script_path}")
+
+ removed_files = []
+
+ # Remove library file if it exists
+ if os.path.exists(self.lib_file):
+ decky.logger.info(f"Found library file, attempting to remove: {self.lib_file}")
+ try:
+ os.remove(self.lib_file)
+ removed_files.append(self.lib_file)
+ decky.logger.info(f"Successfully removed {self.lib_file}")
+ except Exception as e:
+ decky.logger.error(f"Failed to remove {self.lib_file}: {str(e)}")
+ else:
+ decky.logger.info(f"Library file not found: {self.lib_file}")
+
+ # Remove JSON file if it exists
+ if os.path.exists(self.json_file):
+ decky.logger.info(f"Found JSON file, attempting to remove: {self.json_file}")
+ try:
+ os.remove(self.json_file)
+ removed_files.append(self.json_file)
+ decky.logger.info(f"Successfully removed {self.json_file}")
+ except Exception as e:
+ decky.logger.error(f"Failed to remove {self.json_file}: {str(e)}")
+ else:
+ decky.logger.info(f"JSON file not found: {self.json_file}")
+
+ # Remove lsfg script if it exists
+ if os.path.exists(self.lsfg_script_path):
+ decky.logger.info(f"Found lsfg script, attempting to remove: {self.lsfg_script_path}")
+ try:
+ os.remove(self.lsfg_script_path)
+ removed_files.append(self.lsfg_script_path)
+ decky.logger.info(f"Successfully removed {self.lsfg_script_path}")
+ except Exception as e:
+ decky.logger.error(f"Failed to remove {self.lsfg_script_path}: {str(e)}")
+ else:
+ decky.logger.info(f"lsfg script not found: {self.lsfg_script_path}")
+
+ if removed_files:
+ decky.logger.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}")
+ else:
+ decky.logger.info("No lsfg-vk files found to clean up during plugin uninstall")
+
+ except Exception as e:
+ decky.logger.error(f"Error cleaning up lsfg-vk files during uninstall: {str(e)}")
+ import traceback
+ decky.logger.error(f"Traceback: {traceback.format_exc()}")