Skip to content

commit 965243b

abduznik edited this page May 23, 2026 · 1 revision

feat: add RetroDECK support and refactor Linux environment strategies

Commit: 965243b109f208658515180b01ff2c4cd6b6efde

Author: abduznik

Date: 2026-04-17

Why: Adds a new feature or capability to the application.

Files Changed

.../linux_strategies/emudeck_strategy.dart         | 169 ++++++++++++++++++++
 .../linux_environment_strategy.dart                |  31 ++++
 .../linux_strategies/native_linux_strategy.dart    |  55 +++++++
 .../linux_strategies/retrodeck_strategy.dart       |  63 ++++++++
 lib/core/emulator/strategies/azahar_strategy.dart  |  66 +-------
 lib/core/emulator/strategies/cemu_strategy.dart    |  43 +----
 lib/core/emulator/strategies/dolphin_strategy.dart |  87 ++--------
 .../emulator/strategies/duckstation_strategy.dart  |  51 +-----
 lib/core/emulator/strategies/eden_strategy.dart    | 135 +++-------------
 lib/core/emulator/strategies/flycast_strategy.dart |  51 +-----
 lib/core/emulator/strategies/mame_strategy.dart    |  39 +----
 lib/core/emulator/strategies/melonds_strategy.dart |  54 +------
 lib/core/emulator/strategies/mgba_strategy.dart    |  47 +-----
 lib/core/emulator/strategies/pcsx2_strategy.dart   |  73 ++-------
 lib/core/emulator/strategies/ppsspp_strategy.dart  |  46 +-----
 .../emulator/strategies/retroarch_strategy.dart    |  62 +-------
 lib/core/emulator/strategies/rpcs3_strategy.dart   |  44 +----
 lib/core/emulator/strategies/xemu_strategy.dart    |  29 +---
 lib/core/emulator/strategies/xenia_strategy.dart   |  27 +---
 .../save/strategies/dolphin_save_strategy.dart     |   8 +-
 lib/core/save/strategies/pcsx2_save_strategy.dart  |  11 +-
 .../save/strategies/retroarch_save_strategy.dart   |  34 +++-
 lib/core/storage/directory_service.dart            | 177 ++++++++++++---------
 lib/ui/screens/settings_screen.dart                |  88 ++++++++++
 macos/Runner.xcodeproj/project.pbxproj             |   4 +
 25 files changed, 656 insertions(+), 838 deletions(-)
  • lib/core/emulator/linux_strategies/emudeck_strategy.dart
  • lib/core/emulator/linux_strategies/linux_environment_strategy.dart
  • lib/core/emulator/linux_strategies/native_linux_strategy.dart
  • lib/core/emulator/linux_strategies/retrodeck_strategy.dart
  • lib/core/emulator/strategies/azahar_strategy.dart
  • lib/core/emulator/strategies/cemu_strategy.dart
  • lib/core/emulator/strategies/dolphin_strategy.dart
  • lib/core/emulator/strategies/duckstation_strategy.dart
  • lib/core/emulator/strategies/eden_strategy.dart
  • lib/core/emulator/strategies/flycast_strategy.dart
  • lib/core/emulator/strategies/mame_strategy.dart
  • lib/core/emulator/strategies/melonds_strategy.dart
  • lib/core/emulator/strategies/mgba_strategy.dart
  • lib/core/emulator/strategies/pcsx2_strategy.dart
  • lib/core/emulator/strategies/ppsspp_strategy.dart
  • lib/core/emulator/strategies/retroarch_strategy.dart
  • lib/core/emulator/strategies/rpcs3_strategy.dart
  • lib/core/emulator/strategies/xemu_strategy.dart
  • lib/core/emulator/strategies/xenia_strategy.dart
  • lib/core/save/strategies/dolphin_save_strategy.dart
  • lib/core/save/strategies/pcsx2_save_strategy.dart
  • lib/core/save/strategies/retroarch_save_strategy.dart
  • lib/core/storage/directory_service.dart
  • lib/ui/screens/settings_screen.dart
  • macos/Runner.xcodeproj/project.pbxproj

Diff

diff --git a/lib/core/emulator/linux_strategies/emudeck_strategy.dart b/lib/core/emulator/linux_strategies/emudeck_strategy.dart
new file mode 100644
index 0000000..58d0404
--- /dev/null
+++ b/lib/core/emulator/linux_strategies/emudeck_strategy.dart
@@ -0,0 +1,169 @@
+import 'dart:io' as io;
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'linux_environment_strategy.dart';
+
+class EmuDeckStrategy extends LinuxEnvironmentStrategy {
+  @override
+  String get name => 'EmuDeck';
+
+  @override
+  String get id => 'emudeck';
+
+  @override
+  String getRomsRoot(String home, String? customPath, String? emudeckRoot) {
+    if (emudeckRoot != null) {
+      return customPath ?? p.join(emudeckRoot, 'Emulation/roms');
+    }
+    return customPath ?? p.join(home, 'ROMs');
+  }
+
+  @override
+  String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot) {
+    if (emudeckRoot != null) {
+      return customPath ?? p.join(emudeckRoot, 'Emulation/tools');
+    }
+    return customPath ?? p.join(home, 'Emulators');
+  }
+
+  @override
+  String getEmulatorAppSupportDirectory(String home, String emulatorName, String? emudeckRoot, {String? platformSlug}) {
+    if (emudeckRoot != null) {
+      // EmuDeck folder names mapping (some are capitalized)
+      final Map<String, String> emudeckSavesMap = {
+        'cemu': 'Cemu',
+        'vita3k': 'Vita3K',
+        'mame': 'MAME',
+      };
+
+      final emuFolderName = emudeckSavesMap[emulatorName.toLowerCase()] ?? emulatorName.toLowerCase();
+      final base = p.join(emudeckRoot, 'Emulation', 'saves', emuFolderName);
+
+      if (platformSlug != null) {
+        final slug = platformSlug.toLowerCase();
+        if (emulatorName.toLowerCase() == 'dolphin' || emulatorName.toLowerCase() == 'primehack') {
+          if (slug == 'gc' || slug == 'gamecube' || slug == 'ngc') return p.join(base, 'GC');
+          if (slug == 'wii') return p.join(base, 'Wii');
+        }
+      }
+
+      return platformSlug != null ? p.join(base, platformSlug) : base;
+    }
+    return p.join(home, '.config', emulatorName);
+  }
+
+  @override
+  String getBiosPath(String home, String? emudeckRoot) {
+    if (emudeckRoot != null) {
+      return p.join(emudeckRoot, 'Emulation', 'bios');
+    }
+    return p.join(home, 'Emulators', 'BIOS');
+  }
+
+  @override
+  Future<String?> findExecutable(String emulatorId, String executableName, String emulatorsRoot, String? emudeckRoot) async {
+    if (emudeckRoot != null) {
+      final masterLauncher = io.File(p.join(emudeckRoot, 'Emulation', 'tools', 'emu-launch.sh'));
+      if (await masterLauncher.exists()) {
+        return masterLauncher.path;
+      }
+
+      final Map<String, String> emudeckMap = {
+        'rpcs3': 'rpcs3.sh',
+        'pcsx2': 'pcsx2-qt.sh',
+        'dolphin': 'dolphin-emu.sh',
+        'xemu': 'xemu-emu.sh',
+        'xenia_canary': 'xenia.sh',
+        'citra': 'citra.sh',
+        'azahar': 'azahar.sh',
+        'duckstation': 'duckstation.sh',
+        'melonds': 'melonds.sh',
+        'mgba': 'mgba.sh',
+        'ppsspp': 'ppsspp.sh',
+        'retroarch': 'retroarch.sh',
+        'mame': 'mame.sh',
+        'cemu': 'cemu.sh',
+        'flycast': 'flycast.sh',
+        'vita3k': 'vita3k.sh',
+        'ryujinx': 'ryujinx.sh',
+      };
+
+      final launcherName = emudeckMap[emulatorId] ?? '$emulatorId.sh';
+      final launcherFile = io.File(p.join(emudeckRoot, 'Emulation', 'tools', 'launchers', launcherName));
+      if (await launcherFile.exists()) {
+        return launcherFile.path;
+      }
+    }
+    return null;
+  }
+
+  bool isEmuLaunchScript(String path) => p.basename(path) == 'emu-launch.sh';
+
+  @override
+  Future<void> launch(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (isEmuLaunchScript(exePath)) {
+      // Convert internal emulator ID to EmuDeck expected key if necessary
+      final emuKeyMap = {
+        'pcsx2': 'pcsx2-Qt',
+        'retroarch': 'retroarch',
+        'xemu': 'xemu-emu',
+        'dolphin': 'dolphin-emu',
+        'citra': 'citra-qt',
+        'azahar': 'azahar',
+        'eden': 'eden',
+        'ryujinx': 'ryujinx',
+      };
+      final emuKey = emuKeyMap[emulatorId] ?? emulatorId;
+      await io.Process.start('bash', [exePath, '-e', emuKey, ...args, romPath], mode: io.ProcessStartMode.detached);
+    } else if (exePath.endsWith('.sh')) {
+      await io.Process.start('bash', [exePath, ...args, romPath], mode: io.ProcessStartMode.detached);
+    } else {
+      await io.Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.detached);
+    }
+  }
+
+  @override
+  Future<io.Process?> launchWithHandle(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (isEmuLaunchScript(exePath)) {
+      final emuKeyMap = {
+        'pcsx2': 'pcsx2-Qt',
+        'retroarch': 'retroarch',
+        'xemu': 'xemu-emu',
+        'dolphin': 'dolphin-emu',
+        'citra': 'citra-qt',
+        'azahar': 'azahar',
+        'eden': 'eden',
+        'ryujinx': 'ryujinx',
+      };
+      final emuKey = emuKeyMap[emulatorId] ?? emulatorId;
+      return await io.Process.start('bash', [exePath, '-e', emuKey, ...args, romPath], mode: io.ProcessStartMode.normal);
+    } else if (exePath.endsWith('.sh')) {
+      return await io.Process.start('bash', [exePath, ...args, romPath], mode: io.ProcessStartMode.normal);
+    } else {
+      return await io.Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.normal);
+    }
+  }
+
+  @override
+  Future<void> launchStandalone(String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (isEmuLaunchScript(exePath)) {
+      final emuKeyMap = {
+        'pcsx2': 'pcsx2-Qt',
+        'retroarch': 'retroarch',
+        'xemu': 'xemu-emu',
+        'dolphin': 'dolphin-emu',
+        'citra': 'citra-qt',
+        'azahar': 'azahar',
+        'eden': 'eden',
+        'ryujinx': 'ryujinx',
+      };
+      final emuKey = emuKeyMap[emulatorId] ?? emulatorId;
+      await io.Process.start('bash', [exePath, '-e', emuKey, ...args], mode: io.ProcessStartMode.detached);
+    } else if (exePath.endsWith('.sh')) {
+      await io.Process.start('bash', [exePath, ...args], mode: io.ProcessStartMode.detached);
+    } else {
+      final exeDir = io.File(exePath).parent.path;
+      await io.Process.start(exePath, args, mode: io.ProcessStartMode.detached, workingDirectory: exeDir);
+    }
+  }
+}
diff --git a/lib/core/emulator/linux_strategies/linux_environment_strategy.dart b/lib/core/emulator/linux_strategies/linux_environment_strategy.dart
new file mode 100644
index 0000000..7fa1ad5
--- /dev/null
+++ b/lib/core/emulator/linux_strategies/linux_environment_strategy.dart
@@ -0,0 +1,31 @@
+import 'dart:io' as io;
+import 'package:freegosy/core/romm/romm_models.dart';
+
+abstract class LinuxEnvironmentStrategy {
+  String get name;
+  String get id;
+
+  /// Returns the root ROMs directory for this environment.
+  String getRomsRoot(String home, String? customPath, String? emudeckRoot);
+
+  /// Returns the root emulators/tools directory for this environment.
+  String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot);
+
+  /// Returns the app support (save/config) directory for a specific emulator.
+  String getEmulatorAppSupportDirectory(String home, String emulatorName, String? emudeckRoot, {String? platformSlug});
+
+  /// Returns the BIOS directory for this environment.
+  String getBiosPath(String home, String? emudeckRoot);
+
+  /// Tries to find the executable for an emulator.
+  Future<String?> findExecutable(String emulatorId, String executableName, String emulatorsRoot, String? emudeckRoot);
+
+  /// Launches a game.
+  Future<void> launch(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []});
+
+  /// Launches a game and returns the process handle.
+  Future<io.Process?> launchWithHandle(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []});
+
+  /// Launches the emulator standalone.
+  Future<void> launchStandalone(String emulatorId, String exePath, {List<String> args = const []});
+}
diff --git a/lib/core/emulator/linux_strategies/native_linux_strategy.dart b/lib/core/emulator/linux_strategies/native_linux_strategy.dart
new file mode 100644
index 0000000..596f9ec
--- /dev/null
+++ b/lib/core/emulator/linux_strategies/native_linux_strategy.dart
@@ -0,0 +1,55 @@
+import 'dart:io' as io;
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'linux_environment_strategy.dart';
+
+class NativeLinuxStrategy extends LinuxEnvironmentStrategy {
+  @override
+  String get name => 'Default';
+
+  @override
+  String get id => 'default';
+
+  @override
+  String getRomsRoot(String home, String? customPath, String? emudeckRoot) {
+    return customPath ?? p.join(home, 'ROMs');
+  }
+
+  @override
+  String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot) {
+    return customPath ?? p.join(home, 'Emulators');
+  }
+
+  @override
+  String getEmulatorAppSupportDirectory(String home, String emulatorName, String? emudeckRoot, {String? platformSlug}) {
+    return p.join(home, '.config', emulatorName);
+  }
+
+  @override
+  String getBiosPath(String home, String? emudeckRoot) {
+    return p.join(home, 'Emulators', 'BIOS');
+  }
+
+  @override
+  Future<String?> findExecutable(String emulatorId, String executableName, String emulatorsRoot, String? emudeckRoot) async {
+    final direct = io.File(p.join(emulatorsRoot, executableName));
+    if (await direct.exists()) return direct.path;
+    return null;
+  }
+
+  @override
+  Future<void> launch(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    await io.Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.detached);
+  }
+
+  @override
+  Future<io.Process?> launchWithHandle(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    return await io.Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.normal);
+  }
+
+  @override
+  Future<void> launchStandalone(String emulatorId, String exePath, {List<String> args = const []}) async {
+    final exeDir = io.File(exePath).parent.path;
+    await io.Process.start(exePath, args, mode: io.ProcessStartMode.detached, workingDirectory: exeDir);
+  }
+}
diff --git a/lib/core/emulator/linux_strategies/retrodeck_strategy.dart b/lib/core/emulator/linux_strategies/retrodeck_strategy.dart
new file mode 100644
index 0000000..0bb4589
--- /dev/null
+++ b/lib/core/emulator/linux_strategies/retrodeck_strategy.dart
@@ -0,0 +1,63 @@
+import 'dart:io' as io;
+import 'package:path/path.dart' as p;
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'linux_environment_strategy.dart';
+
+class RetroDeckStrategy extends LinuxEnvironmentStrategy {
+  @override
+  String get name => 'RetroDECK';
+
+  @override
+  String get id => 'retrodeck';
+
+  @override
+  String getRomsRoot(String home, String? customPath, String? emudeckRoot) {
+    return customPath ?? p.join(home, 'retrodeck', 'roms');
+  }
+
+  @override
+  String getEmulatorsRoot(String home, String? customPath, String? emudeckRoot) {
+    return customPath ?? p.join(home, 'retrodeck', 'tools');
+  }
+
+  @override
+  String getEmulatorAppSupportDirectory(String home, String emulatorName, String? emudeckRoot, {String? platformSlug}) {
+    final base = p.join(home, '.var', 'app', 'net.retrodeck.retrodeck', 'config');
+    
+    final Map<String, String> retrodeckMap = {
+      'pcsx2': 'PCSX2',
+      'dolphin': 'dolphin-emu',
+      'ppsspp': 'ppsspp',
+      'retroarch': 'retroarch',
+    };
+
+    final folderName = retrodeckMap[emulatorName.toLowerCase()] ?? emulatorName;
+    return p.join(base, folderName);
+  }
+
+  @override
+  String getBiosPath(String home, String? emudeckRoot) {
+    return p.join(home, '.var', 'app', 'net.retrodeck.retrodeck', 'config', 'bios');
+  }
+
+  @override
+  Future<String?> findExecutable(String emulatorId, String executableName, String emulatorsRoot, String? emudeckRoot) async {
+    // RetroDECK uses a single flatpak command for all emulators
+    return 'retrodeck-flatpak';
+  }
+
+  @override
+  Future<void> launch(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    await io.Process.start('flatpak', ['run', 'net.retrodeck.retrodeck', romPath], mode: io.ProcessStartMode.detached);
+  }
+
+  @override
+  Future<io.Process?> launchWithHandle(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    return await io.Process.start('flatpak', ['run', 'net.retrodeck.retrodeck', romPath], mode: io.ProcessStartMode.normal);
+  }
+
+  @override
+  Future<void> launchStandalone(String emulatorId, String exePath, {List<String> args = const []}) async {
+    await io.Process.start('flatpak', ['run', 'net.retrodeck.retrodeck'], mode: io.ProcessStartMode.detached);
+  }
+}
diff --git a/lib/core/emulator/strategies/azahar_strategy.dart b/lib/core/emulator/strategies/azahar_strategy.dart
index c22f0d2..5fbcb8e 100644
--- a/lib/core/emulator/strategies/azahar_strategy.dart
+++ b/lib/core/emulator/strategies/azahar_strategy.dart
@@ -66,35 +66,10 @@ class AzaharStrategy extends EmulatorStrategy {
     final exePath = await _directoryService.findEmulatorExecutable(
       emulatorId, getExecutableForPlatform(),
     );
-
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isMacOS) {
-      if (exePath.endsWith('.dylib')) {
-        throw Exception('Found Azahar Libretro core. Please switch to RetroArch in Settings to play this game.');
-      }
-
-      // On macOS, launching via 'open -a App.app --args rom' is much more stable than launching internal binary
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await _ensure3dsSetup();
-          await io.Process.run('open', [appBundlePath, '--args', romPath]);
-          return;
-        }
-      }
-    }
-
     await _ensure3dsSetup();
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'azahar', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
-    }
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -102,20 +77,10 @@ class AzaharStrategy extends EmulatorStrategy {
     final exePath = await _directoryService.findEmulatorExecutable(
       emulatorId, getExecutableForPlatform(),
     );
-
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isMacOS && exePath.endsWith('.dylib')) {
-      throw Exception('Found Azahar Libretro core. Please switch to RetroArch in Settings to play this game.');
-    }
-
     await _ensure3dsSetup();
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'azahar', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    }
-    return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -125,32 +90,7 @@ class AzaharStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await io.Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = io.File(exePath).parent.path;
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'azahar'], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else {
-      await Process.start(
-        exePath,
-        [],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    }
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/cemu_strategy.dart b/lib/core/emulator/strategies/cemu_strategy.dart
index 888f457..0ad4457 100644
--- a/lib/core/emulator/strategies/cemu_strategy.dart
+++ b/lib/core/emulator/strategies/cemu_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -37,13 +36,7 @@ class CemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'cemu', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, ['-g', romPath], mode: ProcessStartMode.detached);
-    }
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath, args: ['-g']);
   }
 
   @override
@@ -53,12 +46,7 @@ class CemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'cemu', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    }
-    return await Process.start(exePath, ['-g', romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath, args: ['-g']);
   }
 
   @override
@@ -68,32 +56,7 @@ class CemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'cemu'], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else {
-      await Process.start(
-        exePath,
-        [],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    }
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/dolphin_strategy.dart b/lib/core/emulator/strategies/dolphin_strategy.dart
index b1e6eeb..7c9d91a 100644
--- a/lib/core/emulator/strategies/dolphin_strategy.dart
+++ b/lib/core/emulator/strategies/dolphin_strategy.dart
@@ -39,25 +39,10 @@ class DolphinStrategy extends EmulatorStrategy {
     if (exePath == null) {
       throw Exception('$name not found. Please download it first.');
     }
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start(
-        'bash',
-        [exePath, '-e', 'dolphin-emu', romPath],
-        mode: ProcessStartMode.detached,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start(
-        'bash',
-        [exePath, romPath],
-        mode: ProcessStartMode.detached,
-      );
-    } else {
-      await Process.start(
-        exePath,
-        ['-b', '-e', romPath],
-        mode: ProcessStartMode.detached,
-      );
-    }
+
+    // Dolphin needs -b -e to launch a game in batch mode and exit on close
+    final args = io.Platform.isLinux ? <String>[] : ['-b', '-e'];
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath, args: args);
   }
 
   @override
@@ -69,70 +54,26 @@ class DolphinStrategy extends EmulatorStrategy {
     if (exePath == null) {
       throw Exception('$name not found. Please download it first.');
     }
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start(
-        'bash',
-        [exePath, '-e', 'dolphin-emu', romPath],
-        mode: ProcessStartMode.normal,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start(
-        'bash',
-        [exePath, romPath],
-        mode: ProcessStartMode.normal,
-      );
-    }
-    return await Process.start(
+
+    final args = io.Platform.isLinux ? <String>[] : ['-b', '-e'];
+    return await _directoryService.launchGameWithHandle(
+      game,
+      romPath,
+      emulatorId,
       exePath,
-      ['-b', '-e', romPath],
-      mode: ProcessStartMode.normal,
+      args: args,
     );
   }
 
   @override
   Future<void> launchStandalone() async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId, getExecutableForPlatform(),
+      emulatorId,
+      getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    final exeDir = io.File(exePath).parent.path;
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path (folder ending in .app containing the executable)
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start(
-        'bash',
-        [exePath, '-e', 'dolphin-emu'],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start(
-        'bash',
-        [exePath],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    } else {
-      await Process.start(
-        exePath,
-        [],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    }
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/duckstation_strategy.dart b/lib/core/emulator/strategies/duckstation_strategy.dart
index 832ea3c..411dc2a 100644
--- a/lib/core/emulator/strategies/duckstation_strategy.dart
+++ b/lib/core/emulator/strategies/duckstation_strategy.dart
@@ -37,16 +37,7 @@ class DuckstationStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'duckstation', romPath], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-    await Process.start(exePath, ['-batch', romPath], mode: ProcessStartMode.detached);
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath, args: ['-batch']);
   }
 
   @override
@@ -56,14 +47,7 @@ class DuckstationStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'duckstation', romPath], mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-      }
-    }
-    return await Process.start(exePath, ['-batch', romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath, args: ['-batch']);
   }
 
   @override
@@ -73,36 +57,7 @@ class DuckstationStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'duckstation'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(
-      exePath,
-      [],
-      mode: ProcessStartMode.detached,
-      workingDirectory: exeDir,
-    );
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/eden_strategy.dart b/lib/core/emulator/strategies/eden_strategy.dart
index 8e4ef81..d311a8f 100644
--- a/lib/core/emulator/strategies/eden_strategy.dart
+++ b/lib/core/emulator/strategies/eden_strategy.dart
@@ -32,72 +32,38 @@ class EdenStrategy extends EmulatorStrategy {
   @override
   Future<void> launch(Game game, String romPath) async {
     final resolvedPath = _resolveRomPath(romPath);
-    final exePath = await _directoryService.findEmulatorExecutable(emulatorId, getExecutableForPlatform());
-    if (exePath == null) return;
-
-    String? workingDir;
-    if (io.Platform.isMacOS) {
-      workingDir = io.File(exePath).parent.path;
-    }
-
-    final args = [resolvedPath];
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await io.Process.start(
-        'bash',
-        [exePath, '-e', 'eden', ...args],
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: workingDir,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await io.Process.start(
-        'bash',
-        [exePath, ...args],
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: workingDir,
-      );
-    } else {
-      await io.Process.start(
-        exePath,
-        args,
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: workingDir,
-      );
-    }
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    
+    await _directoryService.launchGame(game, resolvedPath, emulatorId, exePath);
   }
 
   @override
   Future<io.Process?> launchWithHandle(Game game, String romPath) async {
     final resolvedPath = _resolveRomPath(romPath);
-    final exePath = await _directoryService.findEmulatorExecutable(emulatorId, getExecutableForPlatform());
-    if (exePath == null) return null;
-
-    String? workingDir;
-    if (io.Platform.isMacOS) {
-      workingDir = io.File(exePath).parent.path;
-    }
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    
+    return await _directoryService.launchGameWithHandle(game, resolvedPath, emulatorId, exePath);
+  }
 
-    final args = [resolvedPath];
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await io.Process.start(
-        'bash',
-        [exePath, '-e', 'eden', ...args],
-        mode: io.ProcessStartMode.normal,
-        workingDirectory: workingDir,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await io.Process.start(
-        'bash',
-        [exePath, ...args],
-        mode: io.ProcessStartMode.normal,
-        workingDirectory: workingDir,
-      );
-    }
-    return await io.Process.start(
-      exePath,
-      args,
-      mode: io.ProcessStartMode.normal,
-      workingDirectory: workingDir,
+  @override
+  Future<void> launchStandalone() async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
     );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+
+    await _directoryService.launchStandalone(emulatorId, exePath);
+  }
+
+  @override
+  String resolveSavePath(Game game) {
+    return "";
   }
 
   String _resolveRomPath(String romPath) {
@@ -118,55 +84,4 @@ class EdenStrategy extends EmulatorStrategy {
     }
     return romPath;
   }
-
-  @override
-  Future<void> launchStandalone() async {
-    final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId, getExecutableForPlatform(),
-    );
-    if (exePath == null) throw Exception('$name not found. Please download it first.');
-
-    final exeDir = io.File(exePath).parent.path;
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await io.Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await io.Process.start(
-        'bash',
-        [exePath, '-e', 'eden'],
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await io.Process.start(
-        'bash',
-        [exePath],
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    } else {
-      await io.Process.start(
-        exePath,
-        [],
-        mode: io.ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    }
-  }
-
-  @override
-  String resolveSavePath(Game game) {
-    return "";
-  }
 }
diff --git a/lib/core/emulator/strategies/flycast_strategy.dart b/lib/core/emulator/strategies/flycast_strategy.dart
index 78c7baa..b935e33 100644
--- a/lib/core/emulator/strategies/flycast_strategy.dart
+++ b/lib/core/emulator/strategies/flycast_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -33,32 +32,21 @@ class FlycastStrategy extends EmulatorStrategy {
   @override
   Future<void> launch(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId,
-      getExecutableForPlatform(),
+      emulatorId, getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'flycast', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
-    }
+    
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
   Future<Process?> launchWithHandle(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId,
-      getExecutableForPlatform(),
+      emulatorId, getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'flycast', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    }
-    return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
+    
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -68,32 +56,7 @@ class FlycastStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'flycast'], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    } else {
-      await Process.start(
-        exePath,
-        [],
-        mode: ProcessStartMode.detached,
-        workingDirectory: exeDir,
-      );
-    }
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/mame_strategy.dart b/lib/core/emulator/strategies/mame_strategy.dart
index f7e65d3..7a6b7c3 100644
--- a/lib/core/emulator/strategies/mame_strategy.dart
+++ b/lib/core/emulator/strategies/mame_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -34,16 +33,7 @@ class MAMEStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'mame', romPath], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -53,14 +43,7 @@ class MAMEStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'mame', romPath], mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-      }
-    }
-    return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -70,23 +53,7 @@ class MAMEStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'mame'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(
-      exePath,
-      [],
-      mode: ProcessStartMode.detached,
-      workingDirectory: exeDir,
-    );
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/melonds_strategy.dart b/lib/core/emulator/strategies/melonds_strategy.dart
index bf8072a..849ff93 100644
--- a/lib/core/emulator/strategies/melonds_strategy.dart
+++ b/lib/core/emulator/strategies/melonds_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -33,40 +32,21 @@ class MelonDSStrategy extends EmulatorStrategy {
   @override
   Future<void> launch(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId,
-      getExecutableForPlatform(),
+      emulatorId, getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'melonds', romPath], workingDirectory: File(exePath).parent.path, mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, romPath], workingDirectory: File(exePath).parent.path, mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-    await Process.start(exePath, [romPath], workingDirectory: File(exePath).parent.path, mode: ProcessStartMode.detached);
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
   Future<Process?> launchWithHandle(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId,
-      getExecutableForPlatform(),
+      emulatorId, getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
-    final workingDir = File(exePath).parent.path;
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'melonds', romPath], workingDirectory: workingDir, mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, romPath], workingDirectory: workingDir, mode: ProcessStartMode.normal);
-      }
-    }
-    return await Process.start(exePath, [romPath], workingDirectory: workingDir, mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -76,31 +56,7 @@ class MelonDSStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'melonds'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], workingDirectory: exeDir, mode: ProcessStartMode.detached);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/mgba_strategy.dart b/lib/core/emulator/strategies/mgba_strategy.dart
index 2075d3c..48c75e4 100644
--- a/lib/core/emulator/strategies/mgba_strategy.dart
+++ b/lib/core/emulator/strategies/mgba_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -37,16 +36,7 @@ class MGBAStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'mgba', romPath], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -56,14 +46,7 @@ class MGBAStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'mgba', romPath], mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-      }
-    }
-    return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -73,31 +56,7 @@ class MGBAStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'mgba'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/pcsx2_strategy.dart b/lib/core/emulator/strategies/pcsx2_strategy.dart
index 6f98fce..0d12ffc 100644
--- a/lib/core/emulator/strategies/pcsx2_strategy.dart
+++ b/lib/core/emulator/strategies/pcsx2_strategy.dart
@@ -33,80 +33,39 @@ class Pcsx2Strategy extends EmulatorStrategy {
   @override
   Future<void> launch(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId, getExecutableForPlatform(),
+      emulatorId,
+      getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
-    
-    if (io.Platform.isWindows) {
-      final normalizedRom = romPath.replaceAll('/', '\\');
-      final normalizedExe = exePath.replaceAll('/', '\\');
-      await Process.start(normalizedExe, [normalizedRom], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'pcsx2-Qt', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
-    }
+
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
   Future<Process?> launchWithHandle(Game game, String romPath) async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId, getExecutableForPlatform(),
+      emulatorId,
+      getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
-    
-    if (io.Platform.isWindows) {
-      final normalizedRom = romPath.replaceAll('/', '\\');
-      final normalizedExe = exePath.replaceAll('/', '\\');
-      return await Process.start(normalizedExe, [normalizedRom], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'pcsx2-Qt', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    } else {
-      return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
-    }
+
+    return await _directoryService.launchGameWithHandle(
+      game,
+      romPath,
+      emulatorId,
+      exePath,
+    );
   }
 
   @override
   Future<void> launchStandalone() async {
     final exePath = await _directoryService.findEmulatorExecutable(
-      emulatorId, getExecutableForPlatform(),
+      emulatorId,
+      getExecutableForPlatform(),
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'pcsx2-Qt'], mode: ProcessStartMode.detached);
-      return;
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-      return;
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-
-    if (io.Platform.isWindows) {
-      final normalizedExe = exePath.replaceAll('/', '\\');
-      final normalizedDir = exeDir.replaceAll('/', '\\');
-      await Process.start(normalizedExe, [], mode: ProcessStartMode.detached, workingDirectory: normalizedDir);
-    } else {
-      await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
-    }
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/ppsspp_strategy.dart b/lib/core/emulator/strategies/ppsspp_strategy.dart
index fa8855c..70807d9 100644
--- a/lib/core/emulator/strategies/ppsspp_strategy.dart
+++ b/lib/core/emulator/strategies/ppsspp_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -37,16 +36,7 @@ class PPSSPPStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'ppsspp', romPath], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -56,14 +46,7 @@ class PPSSPPStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'ppsspp', romPath], mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-      }
-    }
-    return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -73,30 +56,7 @@ class PPSSPPStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'ppsspp'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    if (io.Platform.isMacOS) {
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/retroarch_strategy.dart b/lib/core/emulator/strategies/retroarch_strategy.dart
index 7f635b2..7bc1ea3 100644
--- a/lib/core/emulator/strategies/retroarch_strategy.dart
+++ b/lib/core/emulator/strategies/retroarch_strategy.dart
@@ -1,5 +1,5 @@
 import 'dart:io' as io show Platform, File, Directory;
-import 'dart:io' show Process, ProcessStartMode;
+import 'dart:io' show Process;
 import 'package:dio/dio.dart';
 import 'package:path/path.dart' as p;
 import 'package:path_provider/path_provider.dart';
@@ -243,18 +243,8 @@ class RetroArchStrategy extends EmulatorStrategy {
       await _ensure3dsSetup();
     }
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'retroarch', normalizedRomPath], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath, normalizedRomPath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
     if (coreName == null) {
-      await Process.start(exePath, [normalizedRomPath], mode: ProcessStartMode.detached);
+      await _directoryService.launchGame(game, normalizedRomPath, emulatorId, exePath);
       return;
     }
 
@@ -270,11 +260,7 @@ class RetroArchStrategy extends EmulatorStrategy {
       );
     }
 
-    await Process.start(
-      exePath,
-      ['-L', corePath, normalizedRomPath],
-      mode: ProcessStartMode.detached,
-    );
+    await _directoryService.launchGame(game, normalizedRomPath, emulatorId, exePath, args: ['-L', corePath]);
   }
 
   @override
@@ -299,16 +285,8 @@ class RetroArchStrategy extends EmulatorStrategy {
       await _ensure3dsSetup();
     }
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        return await Process.start('bash', [exePath, '-e', 'retroarch', normalizedRomPath], mode: ProcessStartMode.normal);
-      } else if (exePath.endsWith('.sh')) {
-        return await Process.start('bash', [exePath, normalizedRomPath], mode: ProcessStartMode.normal);
-      }
-    }
-
     if (coreName == null) {
-      return await Process.start(exePath, [normalizedRomPath], mode: ProcessStartMode.normal);
+      return await _directoryService.launchGameWithHandle(game, normalizedRomPath, emulatorId, exePath);
     }
 
     final corePath = await _resolveCorePath(exePath, coreName);
@@ -323,11 +301,7 @@ class RetroArchStrategy extends EmulatorStrategy {
       );
     }
 
-    return await Process.start(
-      exePath,
-      ['-L', corePath, normalizedRomPath],
-      mode: ProcessStartMode.normal,
-    );
+    return await _directoryService.launchGameWithHandle(game, normalizedRomPath, emulatorId, exePath, args: ['-L', corePath]);
   }
 
   Future<void> downloadCore(String coreName, String coresDir, Dio dio) async {
@@ -371,31 +345,7 @@ class RetroArchStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux) {
-      if (_directoryService.isEmuLaunchScript(exePath)) {
-        await Process.start('bash', [exePath, '-e', 'retroarch'], mode: ProcessStartMode.detached);
-        return;
-      } else if (exePath.endsWith('.sh')) {
-        await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-        return;
-      }
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await io.Directory(appBundlePath).exists()) {
-          await Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = io.File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/rpcs3_strategy.dart b/lib/core/emulator/strategies/rpcs3_strategy.dart
index 6ada03b..9ccccc2 100644
--- a/lib/core/emulator/strategies/rpcs3_strategy.dart
+++ b/lib/core/emulator/strategies/rpcs3_strategy.dart
@@ -1,6 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io;
-import 'package:path/path.dart' as p;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -38,14 +36,7 @@ class Rpcs3Strategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    final normalizedRomPath = p.normalize(romPath);
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'rpcs3', normalizedRomPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, normalizedRomPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, [normalizedRomPath], mode: ProcessStartMode.detached);
-    }
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -55,14 +46,7 @@ class Rpcs3Strategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    final normalizedRomPath = p.normalize(romPath);
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'rpcs3', normalizedRomPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, normalizedRomPath], mode: ProcessStartMode.normal);
-    } else {
-      return await Process.start(exePath, [normalizedRomPath], mode: ProcessStartMode.normal);
-    }
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -72,29 +56,7 @@ class Rpcs3Strategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'rpcs3'], mode: ProcessStartMode.detached);
-      return;
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-      return;
-    }
-
-    if (io.Platform.isMacOS) {
-      // Find the .app bundle path
-      final parts = exePath.split('/');
-      final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
-      if (appIdx != -1) {
-        final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
-        if (await io.Directory(appBundlePath).exists()) {
-          await io.Process.run('open', [appBundlePath]);
-          return;
-        }
-      }
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/xemu_strategy.dart b/lib/core/emulator/strategies/xemu_strategy.dart
index cff969f..8cdf111 100644
--- a/lib/core/emulator/strategies/xemu_strategy.dart
+++ b/lib/core/emulator/strategies/xemu_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io show Platform;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -34,14 +33,7 @@ class XemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'xemu-emu', '-dvd_path', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      // EmuDeck scripts expect the ROM path directly
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, ['-dvd_path', romPath], mode: ProcessStartMode.detached);
-    }
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath, args: ['-dvd_path']);
   }
 
   @override
@@ -51,13 +43,7 @@ class XemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'xemu-emu', '-dvd_path', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    } else {
-      return await Process.start(exePath, ['-dvd_path', romPath], mode: ProcessStartMode.normal);
-    }
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath, args: ['-dvd_path']);
   }
 
   @override
@@ -67,16 +53,7 @@ class XemuStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
     
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'xemu-emu'], mode: ProcessStartMode.detached);
-      return;
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-      return;
-    }
-
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/emulator/strategies/xenia_strategy.dart b/lib/core/emulator/strategies/xenia_strategy.dart
index e4c4c51..552baef 100644
--- a/lib/core/emulator/strategies/xenia_strategy.dart
+++ b/lib/core/emulator/strategies/xenia_strategy.dart
@@ -1,5 +1,4 @@
 import 'dart:io';
-import 'dart:io' as io show Platform;
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
@@ -34,13 +33,7 @@ class XeniaStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'xenia', romPath], mode: ProcessStartMode.detached);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.detached);
-    } else {
-      await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
-    }
+    await _directoryService.launchGame(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -50,13 +43,7 @@ class XeniaStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      return await Process.start('bash', [exePath, '-e', 'xenia', romPath], mode: ProcessStartMode.normal);
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      return await Process.start('bash', [exePath, romPath], mode: ProcessStartMode.normal);
-    } else {
-      return await Process.start(exePath, [romPath], mode: ProcessStartMode.normal);
-    }
+    return await _directoryService.launchGameWithHandle(game, romPath, emulatorId, exePath);
   }
 
   @override
@@ -66,15 +53,7 @@ class XeniaStrategy extends EmulatorStrategy {
     );
     if (exePath == null) throw Exception('$name not found. Please download it first.');
 
-    if (io.Platform.isLinux && _directoryService.isEmuLaunchScript(exePath)) {
-      await Process.start('bash', [exePath, '-e', 'xenia'], mode: ProcessStartMode.detached);
-      return;
-    } else if (io.Platform.isLinux && exePath.endsWith('.sh')) {
-      await Process.start('bash', [exePath], mode: ProcessStartMode.detached);
-      return;
-    }
-    final exeDir = File(exePath).parent.path;
-    await Process.start(exePath, [], mode: ProcessStartMode.detached, workingDirectory: exeDir);
+    await _directoryService.launchStandalone(emulatorId, exePath);
   }
 
   @override
diff --git a/lib/core/save/strategies/dolphin_save_strategy.dart b/lib/core/save/strategies/dolphin_save_strategy.dart
index a34dbeb..faf6c3d 100644
--- a/lib/core/save/strategies/dolphin_save_strategy.dart
+++ b/lib/core/save/strategies/dolphin_save_strategy.dart
@@ -73,7 +73,9 @@ class DolphinSaveStrategy extends SaveStrategy {
   @override
   Future<String?> getSaveDir(Game game, String romPath) async {
     final userDir = await _getUserDir(platformSlug: game.platformSlug);
-    final bool isEmuDeck = io.Platform.isLinux && userDir.contains('Emulation/saves');
+    final bool isIntegratedEnv = io.Platform.isLinux && 
+                                (_directoryService.linuxSyncPreset == 'emudeck' || 
+                                 _directoryService.linuxSyncPreset == 'retrodeck');
 
     final isWii = game.platformSlug?.toLowerCase() == 'wii';
 
@@ -81,7 +83,7 @@ class DolphinSaveStrategy extends SaveStrategy {
       // Wii saves are in Wii/title/00010000/[TITLE_ID_HEX]
       String? titleId = _extractGameId(p.basename(romPath));
       
-      final String wiiBase = (isEmuDeck && p.basename(userDir) == 'Wii')
+      final String wiiBase = (isIntegratedEnv && p.basename(userDir).toLowerCase() == 'wii')
           ? userDir
           : p.join(userDir, 'Wii');
 
@@ -97,7 +99,7 @@ class DolphinSaveStrategy extends SaveStrategy {
     } else {
       // GameCube
       final region = _detectRegion(romPath);
-      final String gcBase = (isEmuDeck && p.basename(userDir) == 'GC')
+      final String gcBase = (isIntegratedEnv && p.basename(userDir).toLowerCase() == 'gc')
           ? userDir
           : p.join(userDir, 'GC');
       
diff --git a/lib/core/save/strategies/pcsx2_save_strategy.dart b/lib/core/save/strategies/pcsx2_save_strategy.dart
index 62ab192..5185e95 100644
--- a/lib/core/save/strategies/pcsx2_save_strategy.dart
+++ b/lib/core/save/strategies/pcsx2_save_strategy.dart
@@ -43,11 +43,15 @@ class Pcsx2SaveStrategy extends SaveStrategy {
       }
     }
 
-    // 2. Linux & EmuDeck prioritized
+    // 2. Linux integration (EmuDeck / RetroDECK)
     if (io.Platform.isLinux) {
       final baseDir = await _directoryService.getEmulatorAppSupportDirectory('pcsx2');
-      if (p.basename(baseDir) == 'saves') {
+      final bool isSteamDeckEnv = _directoryService.linuxSyncPreset == 'emudeck' || 
+                                 _directoryService.linuxSyncPreset == 'retrodeck';
+
+      if (isSteamDeckEnv && (p.basename(baseDir) == 'saves' || p.basename(baseDir) == 'PCSX2')) {
         // EmuDeck mapping returns the folder containing the actual saves/cards
+        // RetroDECK mapping returns the PCSX2 folder which also contains memcards/sstates
         return baseDir;
       }
       
@@ -91,6 +95,9 @@ class Pcsx2SaveStrategy extends SaveStrategy {
     final result = <io.File>[];
 
     // Memory cards
+    // EmuDeck: saves/pcsx2/ (mapped as root)
+    // RetroDECK: PCSX2/memcards/
+    // Native: PCSX2/memcards/
     final memcardsDir = io.Directory(isEmuDeck ? root : p.join(root, 'memcards'));
     if (await memcardsDir.exists()) {
       await for (final entity in memcardsDir.list()) {
diff --git a/lib/core/save/strategies/retroarch_save_strategy.dart b/lib/core/save/strategies/retroarch_save_strategy.dart
index 205f0c5..e8add90 100644
--- a/lib/core/save/strategies/retroarch_save_strategy.dart
+++ b/lib/core/save/strategies/retroarch_save_strategy.dart
@@ -101,8 +101,18 @@ class RetroArchSaveStrategy extends SaveStrategy {
     if (io.Platform.isLinux) {
       rootSaveDir = await getSaveDir(game, romPath);
       final baseDir = await _directoryService.getEmulatorAppSupportDirectory('retroarch', platformSlug: slug);
-      // states folder is next to saves folder in EmuDeck structure
-      statesRoot = p.join(p.dirname(baseDir), 'states', coreInfo.statesFolder);
+      
+      if (_directoryService.linuxSyncPreset == 'emudeck') {
+        // EmuDeck: saves are in Emulation/saves/retroarch, states in Emulation/states/retroarch
+        // baseDir is .../Emulation/saves/retroarch
+        final emulationRoot = p.dirname(p.dirname(baseDir));
+        statesRoot = p.join(emulationRoot, 'states', 'retroarch', coreInfo.statesFolder);
+      } else if (_directoryService.linuxSyncPreset == 'retrodeck') {
+        // RetroDECK: baseDir is .../retroarch/
+        statesRoot = p.join(baseDir, 'states', coreInfo.statesFolder);
+      } else {
+        statesRoot = p.join(p.dirname(baseDir), 'states', coreInfo.statesFolder);
+      }
     } else {
       final exePath = await _directoryService.findEmulatorExecutable('retroarch', 'RetroArch.exe');
       if (exePath == null) return {};
@@ -234,9 +244,23 @@ class RetroArchSaveStrategy extends SaveStrategy {
 
       if (io.Platform.isLinux) {
         final baseDir = await _directoryService.getEmulatorAppSupportDirectory('retroarch', platformSlug: slug);
-        targetDir = isState
-            ? p.join(p.dirname(baseDir), 'states', coreInfo.statesFolder)
-            : p.join(baseDir, coreInfo.saveFolder);
+        
+        if (_directoryService.linuxSyncPreset == 'emudeck') {
+          if (isState) {
+            final emulationRoot = p.dirname(p.dirname(baseDir));
+            targetDir = p.join(emulationRoot, 'states', 'retroarch', coreInfo.statesFolder);
+          } else {
+            targetDir = p.join(baseDir, coreInfo.saveFolder);
+          }
+        } else if (_directoryService.linuxSyncPreset == 'retrodeck') {
+          targetDir = isState
+              ? p.join(baseDir, 'states', coreInfo.statesFolder)
+              : p.join(baseDir, 'saves', coreInfo.saveFolder);
+        } else {
+          targetDir = isState
+              ? p.join(p.dirname(baseDir), 'states', coreInfo.statesFolder)
+              : p.join(baseDir, coreInfo.saveFolder);
+        }
       } else {
         final exePath = await _directoryService.findEmulatorExecutable('retroarch', 'RetroArch.exe');
         if (exePath == null) return false;
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index 10e2b11..7f1cebe 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -6,6 +6,10 @@ import 'package:path_provider/path_provider.dart';
 import 'package:path/path.dart' as p;
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/emulator/linux_strategies/linux_environment_strategy.dart';
+import 'package:freegosy/core/emulator/linux_strategies/native_linux_strategy.dart';
+import 'package:freegosy/core/emulator/linux_strategies/emudeck_strategy.dart';
+import 'package:freegosy/core/emulator/linux_strategies/retrodeck_strategy.dart';
 
 enum StorageError { none, pathNotFound, permissionDenied, unknown }
 
@@ -55,24 +59,45 @@ class DirectoryService {
   String? emudeckRootPath;
   final Map<String, String> _emulatorPathOverrides = {};
   StorageStatus status = const StorageStatus();
+  
+  LinuxEnvironmentStrategy? _linuxStrategy;
 
   DirectoryService();
 
+  LinuxEnvironmentStrategy get activeLinuxEnvironment {
+    if (_linuxStrategy != null) return _linuxStrategy!;
+    
+    switch (linuxSyncPreset) {
+      case 'emudeck':
+        _linuxStrategy = EmuDeckStrategy();
+        break;
+      case 'retrodeck':
+        _linuxStrategy = RetroDeckStrategy();
+        break;
+      default:
+        _linuxStrategy = NativeLinuxStrategy();
+    }
+    return _linuxStrategy!;
+  }
+
   Future<StorageStatus> initialize() async {
     try {
       final prefs = await SharedPreferences.getInstance();
       linuxSyncPreset = prefs.getString(_linuxSyncPresetKey) ?? 'default';
       emudeckRootPath = prefs.getString(_emudeckRootPathKey);
+      
+      // Reset strategy to force re-instantiation with correct preset
+      _linuxStrategy = null;
 
       final String defaultBase = await getDefaultBase();
+      final home = io.Platform.environment['HOME'] ?? '';
 
-      if (defaultTargetPlatform == TargetPlatform.linux &&
-          linuxSyncPreset == 'emudeck' &&
-          emudeckRootPath != null) {
-        romsRootPath =
-            prefs.getString(_romsRootPathKey) ?? p.join(emudeckRootPath!, 'Emulation/roms');
-        emulatorsRootPath =
-            prefs.getString(_emulatorsRootPathKey) ?? p.join(emudeckRootPath!, 'Emulation/tools');
+      if (defaultTargetPlatform == TargetPlatform.linux) {
+        final customRoms = prefs.getString(_romsRootPathKey);
+        final customEmus = prefs.getString(_emulatorsRootPathKey);
+        
+        romsRootPath = activeLinuxEnvironment.getRomsRoot(home, customRoms, emudeckRootPath);
+        emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, customEmus, emudeckRootPath);
       } else {
         romsRootPath = prefs.getString(_romsRootPathKey) ?? '$defaultBase/ROMs';
         emulatorsRootPath =
@@ -115,10 +140,10 @@ class DirectoryService {
     final prefs = await SharedPreferences.getInstance();
     await prefs.remove(_romsRootPathKey);
     final base = await getDefaultBase();
-    if (defaultTargetPlatform == TargetPlatform.linux &&
-        linuxSyncPreset == 'emudeck' &&
-        emudeckRootPath != null) {
-      romsRootPath = p.join(emudeckRootPath!, 'Emulation/roms');
+    final home = io.Platform.environment['HOME'] ?? '';
+
+    if (defaultTargetPlatform == TargetPlatform.linux) {
+      romsRootPath = activeLinuxEnvironment.getRomsRoot(home, null, emudeckRootPath);
     } else {
       romsRootPath = '$base/ROMs';
     }
@@ -129,10 +154,10 @@ class DirectoryService {
     final prefs = await SharedPreferences.getInstance();
     await prefs.remove(_emulatorsRootPathKey);
     final base = await getDefaultBase();
-    if (defaultTargetPlatform == TargetPlatform.linux &&
-        linuxSyncPreset == 'emudeck' &&
-        emudeckRootPath != null) {
-      emulatorsRootPath = p.join(emudeckRootPath!, 'Emulation/tools');
+    final home = io.Platform.environment['HOME'] ?? '';
+
+    if (defaultTargetPlatform == TargetPlatform.linux) {
+      emulatorsRootPath = activeLinuxEnvironment.getEmulatorsRoot(home, null, emudeckRootPath);
     } else {
       emulatorsRootPath = '$base/Emulators';
     }
@@ -421,36 +446,16 @@ class DirectoryService {
       final appData = io.Platform.environment['APPDATA'] ?? '';
       return p.join(appData, emulatorName);
     } else if (io.Platform.isLinux) {
-      if (linuxSyncPreset == 'emudeck' && emudeckRootPath != null) {
-        // EmuDeck folder names mapping (some are capitalized)
-        final Map<String, String> emudeckSavesMap = {
-          'cemu': 'Cemu',
-          'vita3k': 'Vita3K',
-          'mame': 'MAME',
-        };
-
-        final emuFolderName = emudeckSavesMap[emulatorName.toLowerCase()] ?? emulatorName.toLowerCase();
-        final base = p.join(emudeckRootPath!, 'Emulation', 'saves', emuFolderName);
-
-        if (platformSlug != null) {
-          final slug = platformSlug.toLowerCase();
-          if (emulatorName.toLowerCase() == 'dolphin' || emulatorName.toLowerCase() == 'primehack') {
-            if (slug == 'gc' || slug == 'gamecube' || slug == 'ngc') return p.join(base, 'GC');
-            if (slug == 'wii') return p.join(base, 'Wii');
-          }
-        }
-
-        return platformSlug != null ? p.join(base, platformSlug) : base;
-      }
       final home = io.Platform.environment['HOME'] ?? '';
-      return p.join(home, '.config', emulatorName);
+      return activeLinuxEnvironment.getEmulatorAppSupportDirectory(home, emulatorName, emudeckRootPath, platformSlug: platformSlug);
     }
     throw UnsupportedError('Platform not supported for save path resolution');
   }
 
   Future<String> getEmulatorBiosDirectory(String emulatorId, {String? platformSlug}) async {
-    if (io.Platform.isLinux && linuxSyncPreset == 'emudeck' && emudeckRootPath != null) {
-      return p.join(emudeckRootPath!, 'Emulation', 'bios');
+    if (io.Platform.isLinux) {
+      final home = io.Platform.environment['HOME'] ?? '';
+      return activeLinuxEnvironment.getBiosPath(home, emudeckRootPath);
     }
     final emuDir = await getEmulatorDirectory(emulatorId);
     final dirPath = p.join(emuDir, 'BIOS');
@@ -493,41 +498,9 @@ class DirectoryService {
       return direct.path;
     }
 
-    // EmuDeck support: Check for master emu-launch.sh first
-    if (io.Platform.isLinux && emudeckRootPath != null) {
-      final masterLauncher = File(p.join(emudeckRootPath!, 'Emulation', 'tools', 'emu-launch.sh'));
-      if (await masterLauncher.exists()) {
-        debugPrint("[DirectoryService] Found EmuDeck master launcher: ${masterLauncher.path}");
-        return masterLauncher.path;
-      }
-
-      // Fallback to specific .sh launchers in [ROOT]/tools/launchers
-      final Map<String, String> emudeckMap = {
-        'rpcs3': 'rpcs3.sh',
-        'pcsx2': 'pcsx2-qt.sh',
-        'dolphin': 'dolphin-emu.sh',
-        'xemu': 'xemu-emu.sh',
-        'xenia_canary': 'xenia.sh',
-        'citra': 'citra.sh',
-        'azahar': 'azahar.sh',
-        'duckstation': 'duckstation.sh',
-        'melonds': 'melonds.sh',
-        'mgba': 'mgba.sh',
-        'ppsspp': 'ppsspp.sh',
-        'retroarch': 'retroarch.sh',
-        'mame': 'mame.sh',
-        'cemu': 'cemu.sh',
-        'flycast': 'flycast.sh',
-        'vita3k': 'vita3k.sh',
-        'ryujinx': 'ryujinx.sh',
-      };
-
-      final launcherName = emudeckMap[emulatorId] ?? '$emulatorId.sh';
-      final launcherFile = File(p.join(emudeckRootPath!, 'Emulation', 'tools', 'launchers', launcherName));
-      if (await launcherFile.exists()) {
-        debugPrint("[DirectoryService] Found EmuDeck launcher: ${launcherFile.path}");
-        return launcherFile.path;
-      }
+    if (io.Platform.isLinux) {
+      final envPath = await activeLinuxEnvironment.findExecutable(emulatorId, executableName, emulatorsRootPath, emudeckRootPath);
+      if (envPath != null) return envPath;
     }
 
     if (executableName.contains('/')) {
@@ -638,6 +611,62 @@ class DirectoryService {
     return p.basename(path) == 'emu-launch.sh';
   }
 
+  Future<void> launchGame(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (io.Platform.isLinux) {
+      await activeLinuxEnvironment.launch(game, romPath, emulatorId, exePath, args: args);
+    } else {
+      if (io.Platform.isWindows) {
+        final normalizedRom = romPath.replaceAll('/', '\\');
+        final normalizedExe = exePath.replaceAll('/', '\\');
+        await Process.start(normalizedExe, [...args, normalizedRom], mode: io.ProcessStartMode.detached);
+      } else {
+        await Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.detached);
+      }
+    }
+  }
+
+  Future<Process?> launchGameWithHandle(Game game, String romPath, String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (io.Platform.isLinux) {
+      return await activeLinuxEnvironment.launchWithHandle(game, romPath, emulatorId, exePath, args: args);
+    } else {
+      if (io.Platform.isWindows) {
+        final normalizedRom = romPath.replaceAll('/', '\\');
+        final normalizedExe = exePath.replaceAll('/', '\\');
+        return await Process.start(normalizedExe, [...args, normalizedRom], mode: io.ProcessStartMode.normal);
+      } else {
+        return await Process.start(exePath, [...args, romPath], mode: io.ProcessStartMode.normal);
+      }
+    }
+  }
+
+  Future<void> launchStandalone(String emulatorId, String exePath, {List<String> args = const []}) async {
+    if (io.Platform.isLinux) {
+      await activeLinuxEnvironment.launchStandalone(emulatorId, exePath, args: args);
+    } else {
+      if (io.Platform.isMacOS) {
+        // Find the .app bundle path
+        final parts = exePath.split('/');
+        final appIdx = parts.indexWhere((p) => p.endsWith('.app'));
+        if (appIdx != -1) {
+          final appBundlePath = parts.sublist(0, appIdx + 1).join('/');
+          if (await Directory(appBundlePath).exists()) {
+            await io.Process.run('open', [appBundlePath]);
+            return;
+          }
+        }
+      }
+
+      final exeDir = File(exePath).parent.path;
+      if (io.Platform.isWindows) {
+        final normalizedExe = exePath.replaceAll('/', '\\');
+        final normalizedDir = exeDir.replaceAll('/', '\\');
+        await Process.start(normalizedExe, args, mode: io.ProcessStartMode.detached, workingDirectory: normalizedDir);
+      } else {
+        await Process.start(exePath, args, mode: io.ProcessStartMode.detached, workingDirectory: exeDir);
+      }
+    }
+  }
+
   Future<void> deleteRom(Game game) async {
     final path = await findExistingRomPath(game);
     if (path != null) {
diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart
index 2b650cc..0802244 100644
--- a/lib/ui/screens/settings_screen.dart
+++ b/lib/ui/screens/settings_screen.dart
@@ -1,3 +1,5 @@
+import 'dart:io' as io;
+import 'package:path/path.dart' as p;
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -514,6 +516,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
           segments: const [
             ButtonSegment(value: 'default', label: Text('Default')),
             ButtonSegment(value: 'emudeck', label: Text('EmuDeck')),
+            ButtonSegment(value: 'retrodeck', label: Text('RetroDECK')),
           ],
           selected: {directoryService.linuxSyncPreset},
           onSelectionChanged: (selection) async {
@@ -542,10 +545,95 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
             style: TextStyle(fontSize: 12, color: Colors.grey, fontStyle: FontStyle.italic),
           ),
         ],
+        if (directoryService.linuxSyncPreset == 'retrodeck') ...[
+          const SizedBox(height: 16),
+          const Text(
+            'RetroDECK integration is active. Games will be launched via flatpak.',
+            style: TextStyle(fontSize: 14, color: Colors.deepPurpleAccent),
+          ),
+          const SizedBox(height: 8),
+          const Text(
+            'Default paths: ~/retrodeck/roms/ for ROMs and ~/.var/app/net.retrodeck.retrodeck/config/ for saves.',
+            style: TextStyle(fontSize: 12, color: Colors.grey, fontStyle: FontStyle.italic),
+          ),
+        ],
+        // Detection suggestions
+        _buildDetectionSuggestions(directoryService),
       ],
     );
   }
 
+  Widget _buildDetectionSuggestions(DirectoryService directoryService) {
+    return FutureBuilder<Map<String, bool>>(
+      future: () async {
+        if (defaultTargetPlatform != TargetPlatform.linux) return <String, bool>{};
+        
+        bool retrodeckFound = false;
+        try {
+          final result = await io.Process.run('flatpak', ['info', 'net.retrodeck.retrodeck']);
+          retrodeckFound = result.exitCode == 0;
+        } catch (_) {}
+
+        final home = io.Platform.environment['HOME'] ?? '';
+        final emudeckFound = await io.Directory(p.join(home, 'Emulation', 'roms')).exists();
+
+        return <String, bool>{'retrodeck': retrodeckFound, 'emudeck': emudeckFound};
+      }(),
+      builder: (context, snapshot) {
+        if (!snapshot.hasData || snapshot.data!.isEmpty) return const SizedBox.shrink();
+        
+        final found = snapshot.data!;
+        if (!found['retrodeck']! && !found['emudeck']!) return const SizedBox.shrink();
+
+        final current = directoryService.linuxSyncPreset;
+        String? suggestion;
+        String? suggestionId;
+
+        if (found['retrodeck']! && current != 'retrodeck') {
+          suggestion = 'RetroDECK';
+          suggestionId = 'retrodeck';
+        } else if (found['emudeck']! && current == 'default') {
+          suggestion = 'EmuDeck';
+          suggestionId = 'emudeck';
+        }
+
+        if (suggestion == null) return const SizedBox.shrink();
+
+        return Padding(
+          padding: const EdgeInsets.only(top: 16),
+          child: Container(
+            padding: const EdgeInsets.all(12),
+            decoration: BoxDecoration(
+              color: Colors.deepPurple.withValues(alpha: 0.1),
+              borderRadius: BorderRadius.circular(8),
+              border: Border.all(color: Colors.deepPurple.withValues(alpha: 0.3)),
+            ),
+            child: Row(
+              children: [
+                const Icon(Icons.info_outline, size: 20, color: Colors.deepPurpleAccent),
+                const SizedBox(width: 12),
+                Expanded(
+                  child: Text(
+                    'We detected $suggestion on your system. Would you like to switch?',
+                    style: const TextStyle(fontSize: 13),
+                  ),
+                ),
+                TextButton(
+                  onPressed: () async {
+                    await directoryService.setLinuxSyncPreset(suggestionId!);
+                    ref.invalidate(directoryServiceProvider);
+                    if (mounted) setState(() {});
+                  },
+                  child: const Text('Switch'),
+                ),
+              ],
+            ),
+          ),
+        );
+      },
+    );
+  }
+
   // --- Legal Section ---
   Widget _buildLegalSection(BuildContext context) {
     return Column(
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj
index 18540de..7efb5c8 100644
--- a/macos/Runner.xcodeproj/project.pbxproj
+++ b/macos/Runner.xcodeproj/project.pbxproj
@@ -412,10 +412,14 @@
 			inputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
 			);
+			inputPaths = (
+			);
 			name = "[CP] Embed Pods Frameworks";
 			outputFileListPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
 			);
+			outputPaths = (
+			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

Clone this wiki locally