/ src / Ryujinx.UI.Common / Helper / FileAssociationHelper.cs
FileAssociationHelper.cs
  1  using Microsoft.Win32;
  2  using Ryujinx.Common;
  3  using Ryujinx.Common.Logging;
  4  using System;
  5  using System.Diagnostics;
  6  using System.IO;
  7  using System.Runtime.InteropServices;
  8  using System.Runtime.Versioning;
  9  
 10  namespace Ryujinx.UI.Common.Helper
 11  {
 12      public static partial class FileAssociationHelper
 13      {
 14          private static readonly string[] _fileExtensions = { ".nca", ".nro", ".nso", ".nsp", ".xci" };
 15  
 16          [SupportedOSPlatform("linux")]
 17          private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
 18  
 19          private const int SHCNE_ASSOCCHANGED = 0x8000000;
 20          private const int SHCNF_FLUSH = 0x1000;
 21  
 22          [LibraryImport("shell32.dll", SetLastError = true)]
 23          public static partial void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
 24  
 25          public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild;
 26  
 27          [SupportedOSPlatform("linux")]
 28          private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml"));
 29  
 30          [SupportedOSPlatform("linux")]
 31          private static bool InstallLinuxMimeTypes(bool uninstall = false)
 32          {
 33              string installKeyword = uninstall ? "uninstall" : "install";
 34  
 35              if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux()))
 36              {
 37                  string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml");
 38                  string additionalArgs = !uninstall ? "--novendor" : "";
 39  
 40                  using Process mimeProcess = new();
 41  
 42                  mimeProcess.StartInfo.FileName = "xdg-mime";
 43                  mimeProcess.StartInfo.Arguments = $"{installKeyword} {additionalArgs} --mode user {mimeTypesFile}";
 44  
 45                  mimeProcess.Start();
 46                  mimeProcess.WaitForExit();
 47  
 48                  if (mimeProcess.ExitCode != 0)
 49                  {
 50                      Logger.Error?.PrintMsg(LogClass.Application, $"Unable to {installKeyword} mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
 51  
 52                      return false;
 53                  }
 54  
 55                  using Process updateMimeProcess = new();
 56  
 57                  updateMimeProcess.StartInfo.FileName = "update-mime-database";
 58                  updateMimeProcess.StartInfo.Arguments = _mimeDbPath;
 59  
 60                  updateMimeProcess.Start();
 61                  updateMimeProcess.WaitForExit();
 62  
 63                  if (updateMimeProcess.ExitCode != 0)
 64                  {
 65                      Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
 66                  }
 67              }
 68  
 69              return true;
 70          }
 71  
 72          [SupportedOSPlatform("windows")]
 73          private static bool AreMimeTypesRegisteredWindows()
 74          {
 75              static bool CheckRegistering(string ext)
 76              {
 77                  RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}");
 78  
 79                  if (key is null)
 80                  {
 81                      return false;
 82                  }
 83  
 84                  var openCmd = key.OpenSubKey(@"shell\open\command");
 85  
 86                  string keyValue = (string)openCmd.GetValue("");
 87  
 88                  return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName));
 89              }
 90  
 91              bool registered = false;
 92  
 93              foreach (string ext in _fileExtensions)
 94              {
 95                  registered |= CheckRegistering(ext);
 96              }
 97  
 98              return registered;
 99          }
100  
101          [SupportedOSPlatform("windows")]
102          private static bool InstallWindowsMimeTypes(bool uninstall = false)
103          {
104              static bool RegisterExtension(string ext, bool uninstall = false)
105              {
106                  string keyString = @$"Software\Classes\{ext}";
107  
108                  if (uninstall)
109                  {
110                      // If the types don't already exist, there's nothing to do and we can call this operation successful.
111                      if (!AreMimeTypesRegisteredWindows())
112                      {
113                          return true;
114                      }
115                      Logger.Debug?.Print(LogClass.Application, $"Removing type association {ext}");
116                      Registry.CurrentUser.DeleteSubKeyTree(keyString);
117                      Logger.Debug?.Print(LogClass.Application, $"Removed type association {ext}");
118                  }
119                  else
120                  {
121                      using var key = Registry.CurrentUser.CreateSubKey(keyString);
122  
123                      if (key is null)
124                      {
125                          return false;
126                      }
127  
128                      Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}");
129                      using var openCmd = key.CreateSubKey(@"shell\open\command");
130                      openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\"");
131                      Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}");
132  
133                  }
134  
135                  return true;
136              }
137  
138              bool registered = false;
139  
140              foreach (string ext in _fileExtensions)
141              {
142                  registered |= RegisterExtension(ext, uninstall);
143              }
144  
145              // Notify Explorer the file association has been changed.
146              SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
147  
148              return registered;
149          }
150  
151          public static bool AreMimeTypesRegistered()
152          {
153              if (OperatingSystem.IsLinux())
154              {
155                  return AreMimeTypesRegisteredLinux();
156              }
157  
158              if (OperatingSystem.IsWindows())
159              {
160                  return AreMimeTypesRegisteredWindows();
161              }
162  
163              // TODO: Add macOS support.
164  
165              return false;
166          }
167  
168          public static bool Install()
169          {
170              if (OperatingSystem.IsLinux())
171              {
172                  return InstallLinuxMimeTypes();
173              }
174  
175              if (OperatingSystem.IsWindows())
176              {
177                  return InstallWindowsMimeTypes();
178              }
179  
180              // TODO: Add macOS support.
181  
182              return false;
183          }
184  
185          public static bool Uninstall()
186          {
187              if (OperatingSystem.IsLinux())
188              {
189                  return InstallLinuxMimeTypes(true);
190              }
191  
192              if (OperatingSystem.IsWindows())
193              {
194                  return InstallWindowsMimeTypes(true);
195              }
196  
197              // TODO: Add macOS support.
198  
199              return false;
200          }
201      }
202  }