summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 21:09:27 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 21:09:27 -0400
commiteb51ec80231cf13accf8af156fa7a4283e2a2f88 (patch)
tree95fd368b42efa29f28b924c8884a8f0d4c80d123
parent4390d600ffd35184c4c30bc64480f58e468627de (diff)
downloaddecky-lsfg-vk-eb51ec80231cf13accf8af156fa7a4283e2a2f88.tar.gz
decky-lsfg-vk-eb51ec80231cf13accf8af156fa7a4283e2a2f88.zip
fix: harden lsfg-v2 migration and flatpak supportfeat/lsfg-v2-release
-rw-r--r--README.md5
-rw-r--r--py_modules/lsfg_vk/config_schema.py6
-rw-r--r--py_modules/lsfg_vk/config_schema_generated.py8
-rw-r--r--py_modules/lsfg_vk/configuration.py7
-rw-r--r--py_modules/lsfg_vk/flatpak_service.py26
-rw-r--r--py_modules/lsfg_vk/installation.py27
-rw-r--r--scripts/generate_python_boilerplate.py1
-rw-r--r--shared_config.py7
-rw-r--r--src/components/ConfigurationSection.tsx13
-rw-r--r--src/config/configSchema.ts2
-rw-r--r--src/config/generatedConfigSchema.ts10
-rw-r--r--tests/test_lsfg_v2_migration.py123
12 files changed, 215 insertions, 20 deletions
diff --git a/README.md b/README.md
index 6481ffa..a94d703 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ The plugin provides several configuration options to optimize frame generation f
- **Performance Mode**: Uses a lighter processing model
- **Allow FP16**: Enable half-precision acceleration; disable it for older NVIDIA GPUs
- **Active In**: Optionally match a profile to executable names, Wine executables, or process names
+- **Automatic Profile Matching**: Explicitly let lsfg-vk choose an `Active In` profile; otherwise the Decky-selected profile is always forced
- **GPU**: Optionally select the GPU by name, vendor/device ID, or PCI bus ID
- **Disable Frame Generation**: Disable the layer for the next launch without creating an invalid 1x profile
@@ -64,7 +65,7 @@ For per-game feedback and community support, please join the [decky-lsfg-vk Disc
- Ensure you've added `~/lsfg %command%` to your game's launch options
- Check that the Lossless Scaling DLL was detected correctly in the plugin
- Try enabling Performance Mode if you're experiencing crashes
-- Ensure the selected profile has no `Active In` value when you want the launcher to force that profile
+- Disable **Automatic Profile Matching** to force the profile selected in Decky, even when it has an `Active In` value
- Make sure your game is running in fullscreen mode for best results
**Performance issues?**
@@ -84,7 +85,7 @@ The plugin:
- **FPS Multiplier**: Choose 2x, 3x, or 4x frame generation
- **Flow Scale**: Adjust motion estimation quality vs performance
- **Performance Mode**: Use lighter processing for better performance
- - **FP16, Active In, and GPU**: Use v2 profile parameters and native matching
+ - **FP16, Active In, GPU, and explicit automatic matching**: Use v2 profile parameters without ambiguous profile selection
- **Launch workarounds**: Preserve existing DXVK, Gamescope, Zink, MangoHud, and vkBasalt launch options
- **Hot-reloading**: Multiplier, flow scale, and performance-mode changes are reloaded while games run; other changes may require a swapchain recreation or restart
- Easy uninstallation that removes all installed files when no longer needed
diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py
index 258b9dd..f8a9fe6 100644
--- a/py_modules/lsfg_vk/config_schema.py
+++ b/py_modules/lsfg_vk/config_schema.py
@@ -259,9 +259,13 @@ class ConfigurationManager:
if normalized in profile_data["profiles"]:
raise ValueError(f"Profile '{normalized}' already exists")
source = source_profile if source_profile in profile_data["profiles"] else profile_data["current_profile"]
+ new_profile = dict(profile_data["profiles"][source])
+ # A cloned profile may inherit Active In values that collide with its source.
+ # Force the newly selected profile until the user explicitly enables native matching.
+ new_profile["use_native_matching"] = False
return ProfileData(
current_profile=profile_data["current_profile"],
- profiles={**profile_data["profiles"], normalized: dict(profile_data["profiles"][source])},
+ profiles={**profile_data["profiles"], normalized: new_profile},
global_config=dict(profile_data["global_config"]),
)
diff --git a/py_modules/lsfg_vk/config_schema_generated.py b/py_modules/lsfg_vk/config_schema_generated.py
index 4c37c90..9cfda51 100644
--- a/py_modules/lsfg_vk/config_schema_generated.py
+++ b/py_modules/lsfg_vk/config_schema_generated.py
@@ -20,6 +20,7 @@ FLOW_SCALE = "flow_scale"
PERFORMANCE_MODE = "performance_mode"
PACING = "pacing"
ACTIVE_IN = "active_in"
+USE_NATIVE_MATCHING = "use_native_matching"
GPU = "gpu"
DISABLE_LSFGVK = "disable_lsfgvk"
DXVK_FRAME_RATE = "dxvk_frame_rate"
@@ -41,6 +42,7 @@ class ConfigurationData(TypedDict):
performance_mode: bool
pacing: str
active_in: str
+ use_native_matching: bool
gpu: str
disable_lsfgvk: bool
dxvk_frame_rate: int
@@ -68,6 +70,8 @@ def get_script_parsing_logic():
value = value.strip()
# Auto-generated parsing logic:
+ if key == "DECKY_LSFGVK_AUTO_PROFILE":
+ script_values["use_native_matching"] = value == "1"
if key == "DISABLE_LSFGVK":
script_values["disable_lsfgvk"] = value == "1"
if key == "DXVK_FRAME_RATE":
@@ -102,6 +106,8 @@ def get_script_generation_logic():
"""Return the script generation logic as a callable"""
def generate_script_lines(config):
lines = []
+ if config.get("use_native_matching", False):
+ lines.append("export DECKY_LSFGVK_AUTO_PROFILE=1")
if config.get("disable_lsfgvk", False):
lines.append("export DISABLE_LSFGVK=1")
dxvk_frame_rate = config.get("dxvk_frame_rate", 0)
@@ -127,4 +133,4 @@ def get_script_generation_logic():
return generate_script_lines
-ALL_FIELDS = ['dll', 'allow_fp16', 'multiplier', 'flow_scale', 'performance_mode', 'pacing', 'active_in', 'gpu', 'disable_lsfgvk', 'dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt', 'force_enable_vkbasalt', 'enable_wsi', 'enable_zink']
+ALL_FIELDS = ['dll', 'allow_fp16', 'multiplier', 'flow_scale', 'performance_mode', 'pacing', 'active_in', 'use_native_matching', 'gpu', 'disable_lsfgvk', 'dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt', 'force_enable_vkbasalt', 'enable_wsi', 'enable_zink']
diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py
index 446ac3a..6511745 100644
--- a/py_modules/lsfg_vk/configuration.py
+++ b/py_modules/lsfg_vk/configuration.py
@@ -18,10 +18,9 @@ class ConfigurationService(BaseService):
@staticmethod
def _profile_selection_lines(profile_name: str, config: ConfigurationData) -> list[str]:
- """Use native matching when a selected profile declares active_in."""
- active_in = str(config.get("active_in", "")).strip()
- if active_in:
- return ["# active_in is configured; lsfg-vk will select a matching profile automatically."]
+ """Force the selected profile unless the user explicitly opts into native matching."""
+ if config.get("use_native_matching", False):
+ return ["# lsfg-vk will select a matching profile from Active In."]
return [f"export LSFGVK_PROFILE={shlex.quote(profile_name)}"]
def get_config(self) -> ConfigurationResponse:
diff --git a/py_modules/lsfg_vk/flatpak_service.py b/py_modules/lsfg_vk/flatpak_service.py
index 7f7ef07..80dcf5d 100644
--- a/py_modules/lsfg_vk/flatpak_service.py
+++ b/py_modules/lsfg_vk/flatpak_service.py
@@ -8,6 +8,7 @@ from pathlib import Path
from typing import Dict, Any, List, Optional
from .base_service import BaseService
+from .config_schema import ConfigurationManager
from .types import BaseResponse
@@ -66,6 +67,21 @@ class FlatpakService(BaseService):
self.log.debug("Could not read configured DLL path for Flatpak override: %s", error)
return config_path, dll_directory
+ def _remove_legacy_app_overrides(self, app_id: str) -> list[str]:
+ """Remove only the v1 overrides previously created by this plugin."""
+ legacy_dll_path = self.user_home / ".local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll"
+ legacy_overrides = [
+ ["override", "--user", "--unset-env=LSFG_CONFIG", app_id],
+ ["override", "--user", f"--nofilesystem={legacy_dll_path}", app_id],
+ ["override", "--user", f"--nofilesystem={self.lsfg_launch_script_path}", app_id],
+ ]
+ errors = []
+ for args in legacy_overrides:
+ result = self._run_flatpak_command(args, capture_output=True, text=True)
+ if result.returncode != 0:
+ errors.append(f"{' '.join(args[2:-1])}: {result.stderr}")
+ return errors
+
def _get_clean_env(self):
"""Get a clean environment without PyInstaller's bundled libraries"""
env = os.environ.copy()
@@ -382,6 +398,14 @@ class FlatpakService(BaseService):
return self._error_response(FlatpakOverrideResponse, error_msg,
app_id=app_id, operation="set")
+ legacy_errors = self._remove_legacy_app_overrides(app_id)
+ if legacy_errors:
+ self.log.warning(
+ "Applied v2 overrides for %s but could not fully remove v1 overrides: %s",
+ app_id,
+ "; ".join(legacy_errors),
+ )
+
self.log.info(f"Successfully set lsfg-vk overrides for {app_id}")
return self._success_response(FlatpakOverrideResponse,
f"lsfg-vk overrides set for {app_id}",
@@ -427,6 +451,8 @@ class FlatpakService(BaseService):
if result.returncode != 0:
removal_errors.append(f"unset-env: {result.stderr}")
+ removal_errors.extend(self._remove_legacy_app_overrides(app_id))
+
if removal_errors:
self.log.warning(f"Some override removals had issues for {app_id}: {'; '.join(removal_errors)}")
diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py
index cb6b161..7eee848 100644
--- a/py_modules/lsfg_vk/installation.py
+++ b/py_modules/lsfg_vk/installation.py
@@ -32,6 +32,7 @@ class InstallationService(BaseService):
self.json_file = self.local_share_dir / JSON_FILENAME
self.legacy_lib_file = self.local_lib_dir / LEGACY_LIB_FILENAME
self.legacy_json_file = self.local_share_dir / LEGACY_JSON_FILENAME
+ self._config_recovery_backup: Path | None = None
def install(self) -> InstallationResponse:
try:
@@ -49,8 +50,11 @@ class InstallationService(BaseService):
self._create_lsfg_launch_script(profile_data)
self._remove_legacy_layer_files()
+ message = "lsfg-vk v2 installed successfully"
+ if self._config_recovery_backup is not None:
+ message += f"; unsupported config backed up to {self._config_recovery_backup.name}"
self.log.info("lsfg-vk v2 installed successfully from %s", archive_name)
- return self._success_response(InstallationResponse, "lsfg-vk v2 installed successfully")
+ return self._success_response(InstallationResponse, message)
except (OSError, tarfile.TarError, shutil.Error) as error:
self.log.error("Error installing lsfg-vk: %s", error)
return self._error_response(InstallationResponse, str(error), message="")
@@ -136,13 +140,19 @@ class InstallationService(BaseService):
dll_service = DllDetectionService(self.log)
profile_data: ProfileData
was_legacy = False
+ self._config_recovery_backup = None
if self.config_file_path.exists():
content = self.config_file_path.read_text(encoding="utf-8")
was_legacy = ConfigurationManager.is_legacy_v1(content)
try:
profile_data = ConfigurationManager.parse_toml_content_multi_profile(content)
except (ValueError, KeyError, TypeError) as error:
- self.log.warning("Could not parse existing config, using defaults: %s", error)
+ self._config_recovery_backup = self._backup_config(content, "unrecognized")
+ self.log.warning(
+ "Could not parse existing config; saved it to %s and using defaults: %s",
+ self._config_recovery_backup,
+ error,
+ )
default = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
profile_data = ProfileData(
current_profile="decky-lsfg-vk",
@@ -172,12 +182,15 @@ class InstallationService(BaseService):
)
return profile_data
+ def _backup_config(self, content: str, label: str) -> Path:
+ backup_path = self.config_file_path.with_name(f"{self.config_file_path.name}.{label}.bak")
+ if not backup_path.exists():
+ self._write_file(backup_path, content, 0o644)
+ self.log.info("Backed up %s configuration to %s", label, backup_path)
+ return backup_path
+
def _backup_legacy_config(self, content: str) -> None:
- backup_path = self.config_file_path.with_name(f"{self.config_file_path.name}.v1.bak")
- if backup_path.exists():
- return
- self._write_file(backup_path, content, 0o644)
- self.log.info("Backed up v1 configuration to %s", backup_path)
+ self._backup_config(content, "v1")
def _read_script_values(self) -> Dict[str, Any]:
if not self.lsfg_launch_script_path.exists():
diff --git a/scripts/generate_python_boilerplate.py b/scripts/generate_python_boilerplate.py
index 1445b79..5027023 100644
--- a/scripts/generate_python_boilerplate.py
+++ b/scripts/generate_python_boilerplate.py
@@ -35,6 +35,7 @@ def get_env_var_name(field_name: str) -> str:
"disable_steamdeck_mode": "SteamDeck",
"mangohud_workaround": "MANGOHUD",
"disable_lsfgvk": "DISABLE_LSFGVK",
+ "use_native_matching": "DECKY_LSFGVK_AUTO_PROFILE",
"disable_vkbasalt": "DISABLE_VKBASALT",
"force_enable_vkbasalt": "ENABLE_VKBASALT",
"enable_wsi": "ENABLE_GAMESCOPE_WSI",
diff --git a/shared_config.py b/shared_config.py
index c87d01d..77b4c01 100644
--- a/shared_config.py
+++ b/shared_config.py
@@ -61,6 +61,13 @@ CONFIG_SCHEMA_DEF = {
"description": "optional executable or process names, separated by commas",
"location": "profile",
},
+ "use_native_matching": {
+ "name": "use_native_matching",
+ "fieldType": ConfigFieldType.BOOLEAN,
+ "default": False,
+ "description": "let lsfg-vk choose a profile from Active In instead of forcing the selected profile",
+ "location": "script",
+ },
"gpu": {
"name": "gpu",
"fieldType": ConfigFieldType.STRING,
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx
index 7143bb1..538975e 100644
--- a/src/components/ConfigurationSection.tsx
+++ b/src/components/ConfigurationSection.tsx
@@ -4,7 +4,7 @@ import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
import { ConfigurationData } from "../config/configSchema";
import {
ACTIVE_IN, ALLOW_FP16, DISABLE_LSFGVK, DLL, FLOW_SCALE, GPU,
- PERFORMANCE_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE,
+ PERFORMANCE_MODE, USE_NATIVE_MATCHING, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE,
MANGOHUD_WORKAROUND, DISABLE_VKBASALT, FORCE_ENABLE_VKBASALT, ENABLE_WSI, ENABLE_ZINK
} from "../config/generatedConfigSchema";
@@ -164,13 +164,22 @@ export function ConfigurationSection({
<PanelSectionRow>
<TextField
label="Active In"
- description="Executable/process names separated by commas. When set, lsfg-vk matches profiles automatically."
+ description="Executable/process names separated by commas."
value={config.active_in}
onChange={(event) => onConfigChange(ACTIVE_IN, event.currentTarget.value)}
/>
</PanelSectionRow>
<PanelSectionRow>
+ <ToggleField
+ label="Use Automatic Profile Matching"
+ description="Let lsfg-vk choose a matching Active In profile instead of forcing the profile selected in Decky."
+ checked={config.use_native_matching}
+ onChange={(value) => onConfigChange(USE_NATIVE_MATCHING, value)}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
<SliderField
label={`Base FPS Cap${config.dxvk_frame_rate > 0 ? ` (${config.dxvk_frame_rate} FPS)` : " (Off)"}`}
description="Base framerate cap for DirectX games, before frame multiplier. (Requires game restart to apply)"
diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts
index 9c4437d..7b608c3 100644
--- a/src/config/configSchema.ts
+++ b/src/config/configSchema.ts
@@ -7,7 +7,7 @@ export {
getDefaults,
getFieldTypes,
DLL, ALLOW_FP16, MULTIPLIER, FLOW_SCALE, PERFORMANCE_MODE, PACING,
- ACTIVE_IN, GPU, DISABLE_LSFGVK, DXVK_FRAME_RATE, ENABLE_WOW64,
+ ACTIVE_IN, USE_NATIVE_MATCHING, GPU, DISABLE_LSFGVK, DXVK_FRAME_RATE, ENABLE_WOW64,
DISABLE_STEAMDECK_MODE, MANGOHUD_WORKAROUND, DISABLE_VKBASALT,
FORCE_ENABLE_VKBASALT, ENABLE_WSI, ENABLE_ZINK
} from './generatedConfigSchema';
diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts
index 9b7e6d1..5c5df39 100644
--- a/src/config/generatedConfigSchema.ts
+++ b/src/config/generatedConfigSchema.ts
@@ -15,6 +15,7 @@ export const FLOW_SCALE = "flow_scale" as const;
export const PERFORMANCE_MODE = "performance_mode" as const;
export const PACING = "pacing" as const;
export const ACTIVE_IN = "active_in" as const;
+export const USE_NATIVE_MATCHING = "use_native_matching" as const;
export const GPU = "gpu" as const;
export const DISABLE_LSFGVK = "disable_lsfgvk" as const;
export const DXVK_FRAME_RATE = "dxvk_frame_rate" as const;
@@ -78,6 +79,12 @@ export const CONFIG_SCHEMA: Record<string, ConfigField> = {
default: "",
description: "optional executable or process names, separated by commas"
},
+ use_native_matching: {
+ name: "use_native_matching",
+ fieldType: ConfigFieldType.BOOLEAN,
+ default: false,
+ description: "let lsfg-vk choose a profile from Active In instead of forcing the selected profile"
+ },
gpu: {
name: "gpu",
fieldType: ConfigFieldType.STRING,
@@ -149,6 +156,7 @@ export interface ConfigurationData {
performance_mode: boolean;
pacing: string;
active_in: string;
+ use_native_matching: boolean;
gpu: string;
disable_lsfgvk: boolean;
dxvk_frame_rate: number;
@@ -175,6 +183,7 @@ export function getDefaults(): ConfigurationData {
performance_mode: false,
pacing: "none",
active_in: "",
+ use_native_matching: false,
gpu: "",
disable_lsfgvk: false,
dxvk_frame_rate: 0,
@@ -197,6 +206,7 @@ export function getFieldTypes(): Record<string, ConfigFieldType> {
performance_mode: ConfigFieldType.BOOLEAN,
pacing: ConfigFieldType.STRING,
active_in: ConfigFieldType.STRING,
+ use_native_matching: ConfigFieldType.BOOLEAN,
gpu: ConfigFieldType.STRING,
disable_lsfgvk: ConfigFieldType.BOOLEAN,
dxvk_frame_rate: ConfigFieldType.INTEGER,
diff --git a/tests/test_lsfg_v2_migration.py b/tests/test_lsfg_v2_migration.py
index 4909d8b..cb590df 100644
--- a/tests/test_lsfg_v2_migration.py
+++ b/tests/test_lsfg_v2_migration.py
@@ -73,14 +73,38 @@ class ConfigurationMigrationTests(unittest.TestCase):
self.assertIn("LSFGVK_PROFILE=decky-lsfg-vk", script)
self.assertNotIn("LSFG_PROCESS", script)
- def test_active_in_uses_native_v2_tracking_instead_of_forcing_a_profile(self):
+ def test_active_in_does_not_implicitly_override_the_selected_profile(self):
config = ConfigurationManager.get_defaults()
config["active_in"] = "Game.exe, GameThread"
self.assertEqual(
ConfigurationService._profile_selection_lines("decky-lsfg-vk", config),
- ["# active_in is configured; lsfg-vk will select a matching profile automatically."],
+ ["export LSFGVK_PROFILE=decky-lsfg-vk"],
)
+ config["use_native_matching"] = True
+ self.assertEqual(
+ ConfigurationService._profile_selection_lines("decky-lsfg-vk", config),
+ ["# lsfg-vk will select a matching profile from Active In."],
+ )
+ self.assertEqual(
+ ConfigurationManager.parse_script_content("export DECKY_LSFGVK_AUTO_PROFILE=1\n"),
+ {"use_native_matching": True},
+ )
+
+ def test_cloned_profile_disables_native_matching_until_explicitly_enabled(self):
+ config = ConfigurationManager.get_defaults()
+ config["active_in"] = "Game.exe"
+ config["use_native_matching"] = True
+ profile_data = {
+ "current_profile": "decky-lsfg-vk",
+ "profiles": {"decky-lsfg-vk": config},
+ "global_config": {"dll": "", "allow_fp16": True},
+ }
+
+ cloned = ConfigurationManager.create_profile(profile_data, "Other")
+ self.assertEqual(cloned["profiles"]["Other"]["active_in"], "Game.exe")
+ self.assertFalse(cloned["profiles"]["Other"]["use_native_matching"])
+
class InstallerInfrastructureTests(unittest.TestCase):
def test_layer_archive_selection_has_no_legacy_arm_override(self):
@@ -137,6 +161,36 @@ class InstallerInfrastructureTests(unittest.TestCase):
service._create_config_file()
self.assertEqual(len(list(config_dir.glob("conf.toml.v1.bak*"))), 1)
+ def test_unrecognized_configuration_is_backed_up_before_reset(self):
+ from lsfg_vk.installation import InstallationService
+
+ with tempfile.TemporaryDirectory() as directory:
+ home = Path(directory)
+ config_dir = home / ".config" / "lsfg-vk"
+ config_dir.mkdir(parents=True)
+ config_path = config_dir / "conf.toml"
+ unsupported_config = "version = 3\n"
+ config_path.write_text(unsupported_config, encoding="utf-8")
+
+ service = InstallationService.__new__(InstallationService)
+ service.log = mock.Mock()
+ service.user_home = home
+ service.config_dir = config_dir
+ service.config_file_path = config_path
+ service.lsfg_script_path = home / "lsfg"
+ service.lsfg_launch_script_path = home / "lsfg"
+
+ with mock.patch("lsfg_vk.dll_detection.DllDetectionService") as detection_service:
+ detection_service.return_value.check_lossless_scaling_dll.return_value = {"detected": False}
+ service._create_config_file()
+
+ self.assertEqual(
+ (config_dir / "conf.toml.unrecognized.bak").read_text(encoding="utf-8"),
+ unsupported_config,
+ )
+ self.assertIn("version = 2", config_path.read_text(encoding="utf-8"))
+ self.assertEqual(service._config_recovery_backup, config_dir / "conf.toml.unrecognized.bak")
+
def test_writes_replace_the_config_atomically(self):
service = BaseService.__new__(BaseService)
service.log = mock.Mock()
@@ -152,5 +206,70 @@ class InstallerInfrastructureTests(unittest.TestCase):
replace.assert_called_once()
+class FlatpakMigrationTests(unittest.TestCase):
+ def test_v2_override_setup_and_removal_clean_legacy_overrides(self):
+ from lsfg_vk.flatpak_service import FlatpakService
+
+ with tempfile.TemporaryDirectory() as directory:
+ home = Path(directory)
+ config_dir = home / ".config" / "lsfg-vk"
+ config_dir.mkdir(parents=True)
+ config_path = config_dir / "conf.toml"
+ config_path.write_text(
+ ConfigurationManager.generate_toml_content(ConfigurationManager.get_defaults()),
+ encoding="utf-8",
+ )
+
+ service = FlatpakService.__new__(FlatpakService)
+ service.log = mock.Mock()
+ service.user_home = home
+ service.config_dir = config_dir
+ service.config_file_path = config_path
+ service.lsfg_launch_script_path = home / "lsfg"
+ service.check_flatpak_available = mock.Mock(return_value=True)
+ service._run_flatpak_command = mock.Mock(
+ return_value=types.SimpleNamespace(returncode=0, stdout="", stderr="")
+ )
+
+ app_id = "org.example.Game"
+ result = service.set_app_override(app_id)
+ self.assertTrue(result["success"])
+ setup_calls = [call.args[0] for call in service._run_flatpak_command.call_args_list]
+ self.assertIn(
+ ["override", "--user", f"--env=LSFGVK_CONFIG={config_path}", app_id],
+ setup_calls,
+ )
+ self.assertIn(
+ ["override", "--user", "--unset-env=LSFG_CONFIG", app_id],
+ setup_calls,
+ )
+ self.assertIn(
+ [
+ "override",
+ "--user",
+ f"--nofilesystem={home / '.local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll'}",
+ app_id,
+ ],
+ setup_calls,
+ )
+ self.assertIn(
+ ["override", "--user", f"--nofilesystem={home / 'lsfg'}", app_id],
+ setup_calls,
+ )
+
+ service._run_flatpak_command.reset_mock()
+ result = service.remove_app_override(app_id)
+ self.assertTrue(result["success"])
+ removal_calls = [call.args[0] for call in service._run_flatpak_command.call_args_list]
+ self.assertIn(
+ ["override", "--user", "--unset-env=LSFG_CONFIG", app_id],
+ removal_calls,
+ )
+ self.assertIn(
+ ["override", "--user", f"--nofilesystem={home / 'lsfg'}", app_id],
+ removal_calls,
+ )
+
+
if __name__ == "__main__":
unittest.main()