diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj index 12e11385597..5de32e83248 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj +++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj @@ -46,10 +46,6 @@ .\AppxPackagingTlb.dll True - - .\ShObjIdlTlb.dll - True - diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 22f4aea592f..bd09cd9c61a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -228,7 +228,6 @@ private void DisableProgram(IProgram programToDelete) public static void StartProcess(Func runProcess, ProcessStartInfo info) { - bool hide; try { runProcess(info); diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs new file mode 100644 index 00000000000..4b8423ed6fe --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + public class ApplicationActivationHelper + { + // Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs + public enum ActivateOptions + { + None = 0x00000000, + DesignMode = 0x00000001, + NoErrorUI = 0x00000002, + NoSplashScreen = 0x00000004, + } + + /// ApplicationActivationManager + [ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IApplicationActivationManager + { + IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); + } + + // Application Activation Manager Class + [ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] + public class ApplicationActivationManager : IApplicationActivationManager + { + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/] + public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs new file mode 100644 index 00000000000..4ded3412a66 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.IO; +using Accessibility; +using System.Runtime.InteropServices.ComTypes; +using System.Security.Policy; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + class ShellLinkHelper + { + [Flags()] + public enum SLGP_FLAGS + { + SLGP_SHORTPATH = 0x1, + SLGP_UNCPRIORITY = 0x2, + SLGP_RAWPATH = 0x4 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct WIN32_FIND_DATAW + { + public uint dwFileAttributes; + public long ftCreationTime; + public long ftLastAccessTime; + public long ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public uint dwReserved0; + public uint dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string cFileName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + } + + [Flags()] + public enum SLR_FLAGS + { + SLR_NO_UI = 0x1, + SLR_ANY_MATCH = 0x2, + SLR_UPDATE = 0x4, + SLR_NOUPDATE = 0x8, + SLR_NOSEARCH = 0x10, + SLR_NOTRACK = 0x20, + SLR_NOLINKINFO = 0x40, + SLR_INVOKE_MSI = 0x80 + } + + + // Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW + /// The IShellLink interface allows Shell links to be created, modified, and resolved + [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] + interface IShellLinkW + { + /// Retrieves the path and file name of a Shell link object + void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); + /// Retrieves the list of item identifiers for a Shell link object + void GetIDList(out IntPtr ppidl); + /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. + void SetIDList(IntPtr pidl); + /// Retrieves the description string for a Shell link object + void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + /// Sets the description for a Shell link object. The description can be any application-defined string + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + /// Retrieves the name of the working directory for a Shell link object + void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + /// Sets the name of the working directory for a Shell link object + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + /// Retrieves the command-line arguments associated with a Shell link object + void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + /// Sets the command-line arguments for a Shell link object + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + /// Retrieves the hot key for a Shell link object + void GetHotkey(out short pwHotkey); + /// Sets a hot key for a Shell link object + void SetHotkey(short wHotkey); + /// Retrieves the show command for a Shell link object + void GetShowCmd(out int piShowCmd); + /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. + void SetShowCmd(int iShowCmd); + /// Retrieves the location (path and index) of the icon for a Shell link object + void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, + int cchIconPath, out int piIcon); + /// Sets the location (path and index) of the icon for a Shell link object + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + /// Sets the relative path to the Shell link object + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + /// Attempts to find the target of a Shell link, even if it has been moved or renamed + void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags); + /// Sets the path and file name of a Shell link object + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } + + [ComImport(), Guid("00021401-0000-0000-C000-000000000046")] + public class ShellLink + { + } + + // To initialize the app description + public String description = String.Empty; + + + // Retrieve the target path using Shell Link + public string retrieveTargetPath(string path) + { + var link = new ShellLink(); + const int STGM_READ = 0; + ((IPersistFile)link).Load(path, STGM_READ); + var hwnd = new _RemotableHandle(); + ((IShellLinkW)link).Resolve(ref hwnd, 0); + + const int MAX_PATH = 260; + StringBuilder buffer = new StringBuilder(MAX_PATH); + + var data = new WIN32_FIND_DATAW(); + ((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH); + var target = buffer.ToString(); + + // To set the app description + if (!String.IsNullOrEmpty(target)) + { + buffer = new StringBuilder(MAX_PATH); + ((IShellLinkW)link).GetDescription(buffer, MAX_PATH); + description = buffer.ToString(); + } + return target; + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs index 5db26aa70e6..209b8f92ce4 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs @@ -13,7 +13,6 @@ using Windows.ApplicationModel; using Windows.Management.Deployment; using AppxPackaing; -using Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using IStream = AppxPackaing.IStream; @@ -350,10 +349,10 @@ public List ContextMenus(IPublicAPI api) private async void Launch(IPublicAPI api) { - var appManager = new ApplicationActivationManager(); + var appManager = new ApplicationActivationHelper.ApplicationActivationManager(); uint unusedPid; const string noArgs = ""; - const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; + const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None; await Task.Run(() => { try diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index fd994aeb347..372d365245f 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -8,11 +7,13 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Win32; -using Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; +using Flow.Launcher.Infrastructure.Logger; +using System.Diagnostics; +using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program.Programs { @@ -23,6 +24,7 @@ public class Win32 : IProgram public string UniqueIdentifier { get; set; } public string IcoPath { get; set; } public string FullPath { get; set; } + public string LnkResolvedPath { get; set; } public string ParentDirectory { get; set; } public string ExecutableName { get; set; } public string Description { get; set; } @@ -31,49 +33,64 @@ public class Win32 : IProgram public string Location => ParentDirectory; private const string ShortcutExtension = "lnk"; - private const string ApplicationReferenceExtension = "appref-ms"; private const string ExeExtension = "exe"; public Result Result(string query, IPublicAPI api) { string title; - MatchResult matchResult; + + var nameMatchResult = StringMatcher.FuzzySearch(query, Name); + var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description); + + var pathMatchResult = new MatchResult(false, 0, new List(), 0); + if (ExecutableName != null) // only lnk program will need this one + pathMatchResult = StringMatcher.FuzzySearch(query, ExecutableName); + + MatchResult matchResult = nameMatchResult; + + if (nameMatchResult.Score < descriptionMatchResult.Score) + matchResult = descriptionMatchResult; + + if (!matchResult.IsSearchPrecisionScoreMet()) + { + if (pathMatchResult.IsSearchPrecisionScoreMet()) + matchResult = pathMatchResult; + else return null; + } // We suppose Name won't be null if (Description == null || Name.StartsWith(Description)) { title = Name; - matchResult = StringMatcher.FuzzySearch(query, title); } else if (Description.StartsWith(Name)) { title = Description; - matchResult = StringMatcher.FuzzySearch(query, Description); } else { title = $"{Name}: {Description}"; - var nameMatch = StringMatcher.FuzzySearch(query, Name); - var desciptionMatch = StringMatcher.FuzzySearch(query, Description); - if (desciptionMatch.Score > nameMatch.Score) + + if (matchResult == descriptionMatchResult) { - for (int i = 0; i < desciptionMatch.MatchData.Count; i++) + for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++) { - desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + matchResult.MatchData[i] += Name.Length + 2; // 2 is ": " } - matchResult = desciptionMatch; } - else matchResult = nameMatch; } - if (!matchResult.Success) return null; - + if (matchResult == pathMatchResult) + { + // path Match won't have valid highlight data + matchResult.MatchData = new List(); + } var result = new Result { Title = title, - SubTitle = FullPath, + SubTitle = LnkResolvedPath ?? FullPath, IcoPath = IcoPath, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, @@ -82,7 +99,7 @@ public Result Result(string query, IPublicAPI api) { var info = new ProcessStartInfo { - FileName = FullPath, + FileName = LnkResolvedPath ?? FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; @@ -144,19 +161,19 @@ public List ContextMenus(IPublicAPI api) Action = _ => { var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs) - ? Main._settings.CustomizedArgs - .Replace("%s",$"\"{ParentDirectory}\"") - .Replace("%f",$"\"{FullPath}\"") - : Main._settings.CustomizedExplorer==Settings.Explorer - ? $"/select,\"{FullPath}\"" - : Settings.ExplorerArgs; + ? Main._settings.CustomizedArgs + .Replace("%s", $"\"{ParentDirectory}\"") + .Replace("%f", $"\"{FullPath}\"") + : Main._settings.CustomizedExplorer == Settings.Explorer + ? $"/select,\"{FullPath}\"" + : Settings.ExplorerArgs; Main.StartProcess(Process.Start, - new ProcessStartInfo( - !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) - ? Main._settings.CustomizedExplorer - : Settings.Explorer, - args)); + new ProcessStartInfo( + !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) + ? Main._settings.CustomizedExplorer + : Settings.Explorer, + args)); return true; }, @@ -167,10 +184,9 @@ public List ContextMenus(IPublicAPI api) } - public override string ToString() { - return ExecutableName; + return Name; } private static Win32 Win32Program(string path) @@ -193,7 +209,7 @@ private static Win32 Win32Program(string path) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|Win32Program|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return new Win32() { Valid = false, Enabled = false }; } @@ -204,27 +220,21 @@ private static Win32 LnkProgram(string path) var program = Win32Program(path); try { - var link = new ShellLink(); - const uint STGM_READ = 0; - ((IPersistFile)link).Load(path, STGM_READ); - var hwnd = new _RemotableHandle(); - link.Resolve(ref hwnd, 0); - const int MAX_PATH = 260; StringBuilder buffer = new StringBuilder(MAX_PATH); + ShellLinkHelper _helper = new ShellLinkHelper(); + string target = _helper.retrieveTargetPath(path); - var data = new _WIN32_FIND_DATAW(); - const uint SLGP_SHORTPATH = 1; - link.GetPath(buffer, buffer.Capacity, ref data, SLGP_SHORTPATH); - var target = buffer.ToString(); if (!string.IsNullOrEmpty(target)) { var extension = Extension(target); if (extension == ExeExtension && File.Exists(target)) { - buffer = new StringBuilder(MAX_PATH); - link.GetDescription(buffer, MAX_PATH); - var description = buffer.ToString(); + program.LnkResolvedPath = program.FullPath; + program.FullPath = Path.GetFullPath(target).ToLower(); + program.ExecutableName = Path.GetFileName(target); + + var description = _helper.description; if (!string.IsNullOrEmpty(description)) { program.Description = description; @@ -239,13 +249,15 @@ private static Win32 LnkProgram(string path) } } } + return program; } catch (COMException e) { // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + - "|Error caused likely due to trying to get the description of the program", e); + "|Error caused likely due to trying to get the description of the program", + e); program.Valid = false; return program; @@ -275,7 +287,7 @@ private static Win32 ExeProgram(string path) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return new Win32() { Valid = false, Enabled = false }; } @@ -284,28 +296,13 @@ private static Win32 ExeProgram(string path) private static IEnumerable ProgramPaths(string directory, string[] suffixes) { if (!Directory.Exists(directory)) - return new string[] { }; - try - { - var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = true - }) - .Where(x => suffixes.Contains(Extension(x))); + return Enumerable.Empty(); - return paths; - } - catch (DirectoryNotFoundException e) - { - ProgramLogger.LogException($"Directory not found {directory}", e); - return new string[] { }; - } - catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + return Directory.EnumerateFiles(directory, "*", new EnumerationOptions { - ProgramLogger.LogException($"Permission denied {directory}", e); - return new string[] { }; - } + IgnoreInaccessible = true, + RecurseSubdirectories = true + }).Where(x => suffixes.Contains(Extension(x))); } private static string Extension(string path) @@ -321,14 +318,13 @@ private static string Extension(string path) } } - private static ParallelQuery UnregisteredPrograms(List sources, string[] suffixes) + private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes) { - var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled) - .SelectMany(s => ProgramPaths(s.Location, suffixes)) - .Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier)) + var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled) + .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x) .Distinct(); - var programs = paths.AsParallel().Select(x => Extension(x) switch + var programs = paths.Select(x => Extension(x) switch { ExeExtension => ExeProgram(x), ShortcutExtension => LnkProgram(x), @@ -339,7 +335,7 @@ private static ParallelQuery UnregisteredPrograms(List StartMenuPrograms(string[] suffixes) + private static IEnumerable StartMenuPrograms(string[] suffixes) { var disabledProgramsList = Main._settings.DisabledProgramSources; @@ -350,53 +346,49 @@ private static ParallelQuery StartMenuPrograms(string[] suffixes) var toFilter = paths1.Concat(paths2); - var programs = toFilter - .AsParallel() - .Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1)) - .Distinct() - .Select(x => Extension(x) switch - { - ShortcutExtension => LnkProgram(x), - _ => Win32Program(x) - }).Where(x => x.Valid); + var programs = ExceptDisabledSource(toFilter.Distinct()) + .Select(x => Extension(x) switch + { + ShortcutExtension => LnkProgram(x), + _ => Win32Program(x) + }).Where(x => x.Valid); return programs; } - private static ParallelQuery AppPathsPrograms(string[] suffixes) + private static IEnumerable AppPathsPrograms(string[] suffixes) { // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; - var programs = new List(); - using (var root = Registry.LocalMachine.OpenSubKey(appPaths)) + + IEnumerable toFilter = Enumerable.Empty(); + + using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths); + using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths); + + if (rootMachine != null) { - if (root != null) - { - programs.AddRange(GetProgramsFromRegistry(root)); - } + toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine)); } - using (var root = Registry.CurrentUser.OpenSubKey(appPaths)) + + if (rootUser != null) { - if (root != null) - { - programs.AddRange(GetProgramsFromRegistry(root)); - } + toFilter = toFilter.Concat(GetPathFromRegistry(rootUser)); } - var disabledProgramsList = Main._settings.DisabledProgramSources; - var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName))); - var filtered = toFilter.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)).Select(t1 => t1); + toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); + + var filtered = ExceptDisabledSource(toFilter); - return filtered; + return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue } - private static IEnumerable GetProgramsFromRegistry(RegistryKey root) + private static IEnumerable GetPathFromRegistry(RegistryKey root) { return root - .GetSubKeyNames() - .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) - .Distinct() - .Select(x => GetProgramFromPath(x)); + .GetSubKeyNames() + .Select(x => GetProgramPathFromRegistrySubKeys(root, x)) + .Distinct(); } private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey) @@ -422,7 +414,7 @@ private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { ProgramLogger.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" + - $"|Permission denied when trying to load the program from {path}", e); + $"|Permission denied when trying to load the program from {path}", e); return string.Empty; } @@ -431,27 +423,74 @@ private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string private static Win32 GetProgramFromPath(string path) { if (string.IsNullOrEmpty(path)) - return new Win32(); + return null; path = Environment.ExpandEnvironmentVariables(path); if (!File.Exists(path)) - return new Win32(); + return null; var entry = Win32Program(path); - entry.ExecutableName = Path.GetFileName(path); return entry; } + public static IEnumerable ExceptDisabledSource(IEnumerable sources) + { + return ExceptDisabledSource(sources, x => x); + } + + public static IEnumerable ExceptDisabledSource(IEnumerable sources, + Func keySelector) + { + return Main._settings.DisabledProgramSources.Count == 0 + ? sources + : ExceptDisabledSourceEnumerable(sources, keySelector); + + static IEnumerable ExceptDisabledSourceEnumerable(IEnumerable elements, + Func selector) + { + var set = Main._settings.DisabledProgramSources.Select(x => x.UniqueIdentifier).ToHashSet(); + + foreach (var element in elements) + { + if (!set.Contains(selector(element))) + yield return element; + } + } + } + + public static IEnumerable DistinctBy(IEnumerable source, Func selector) + { + var set = new HashSet(); + foreach (var item in source) + { + if (set.Add(selector(item))) + yield return item; + } + } + + private static Win32[] ProgramsHasher(IEnumerable programs) + { + return programs.GroupBy(p => p.FullPath.ToLower()) + .SelectMany(g => + { + if (g.Count() > 1) + return DistinctBy(g.Where(p => !string.IsNullOrEmpty(p.Description)), x => x.Description); + return g; + }).ToArray(); + } + + public static Win32[] All(Settings settings) { try { - var programs = new List().AsParallel(); + var programs = Enumerable.Empty(); var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); programs = programs.Concat(unregistered); + if (settings.EnableRegistrySource) { var appPaths = AppPathsPrograms(settings.ProgramSuffixes); @@ -464,7 +503,8 @@ public static Win32[] All(Settings settings) programs = programs.Concat(startMenu); } - return programs.ToArray(); + + return ProgramsHasher(programs.Where(p => p != null)); } #if DEBUG //This is to make developer aware of any unhandled exception and add in handling. catch (Exception e) @@ -478,9 +518,9 @@ public static Win32[] All(Settings settings) { ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e); - return new Win32[0]; + return Array.Empty(); } #endif } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll b/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll deleted file mode 100644 index 83815e40a43..00000000000 Binary files a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll and /dev/null differ diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json index f713a33ece5..93e5079c2d5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json @@ -4,7 +4,7 @@ "Name": "Program", "Description": "Search programs in Flow.Launcher", "Author": "qianlifeng", - "Version": "1.4.0", + "Version": "1.4.1", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",