diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
index b93fb23c9..78c66d604 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
@@ -3,6 +3,7 @@ using System.Text;
using System.Runtime.InteropServices;
using Accessibility;
using System.Runtime.InteropServices.ComTypes;
+using Flow.Launcher.Plugin.Program.Logger;
namespace Flow.Launcher.Plugin.Program.Programs
{
@@ -119,9 +120,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
// To set the app description
if (!String.IsNullOrEmpty(target))
{
- buffer = new StringBuilder(MAX_PATH);
- ((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
- description = buffer.ToString();
+ try
+ {
+ buffer = new StringBuilder(MAX_PATH);
+ ((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
+ description = buffer.ToString();
+ }
+ catch (COMException e)
+ {
+ // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception
+ ProgramLogger.LogException($"|IShellLinkW|retrieveTargetPath|{path}" +
+ "|Error caused likely due to trying to get the description of the program",
+ e);
+ }
buffer.Clear();
((IShellLinkW)link).GetArguments(buffer, MAX_PATH);
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs
new file mode 100644
index 000000000..4f344d89e
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+
+namespace Flow.Launcher.Plugin.Program.Programs
+{
+ // From PT Run
+ ///
+ /// Class to get localized name of shell items like 'My computer'. The localization is based on the 'windows display language'.
+ /// Reused code from https://stackoverflow.com/questions/41423491/how-to-get-localized-name-of-known-folder for the method
+ ///
+ public static class ShellLocalization
+ {
+ internal const uint DONTRESOLVEDLLREFERENCES = 0x00000001;
+ internal const uint LOADLIBRARYASDATAFILE = 0x00000002;
+
+ [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes);
+
+ [DllImport("user32.dll", EntryPoint = "LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
+ internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")]
+ internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
+
+ [DllImport("kernel32.dll", ExactSpelling = true)]
+ internal static extern int FreeLibrary(IntPtr hModule);
+
+ [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW", CharSet = CharSet.Unicode, ExactSpelling = true)]
+ internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize);
+
+ ///
+ /// Returns the localized name of a shell item.
+ ///
+ /// Path to the shell item (e. g. shortcut 'File Explorer.lnk').
+ /// The localized name as string or .
+ public static string GetLocalizedName(string path)
+ {
+ StringBuilder resourcePath = new StringBuilder(1024);
+ StringBuilder localizedName = new StringBuilder(1024);
+ int len, id;
+ len = resourcePath.Capacity;
+
+ // If there is no resource to localize a file name the method returns a non zero value.
+ if (SHGetLocalizedName(path, resourcePath, ref len, out id) == 0)
+ {
+ _ = ExpandEnvironmentStrings(resourcePath.ToString(), resourcePath, resourcePath.Capacity);
+ IntPtr hMod = LoadLibraryEx(resourcePath.ToString(), IntPtr.Zero, DONTRESOLVEDLLREFERENCES | LOADLIBRARYASDATAFILE);
+ if (hMod != IntPtr.Zero)
+ {
+ if (LoadString(hMod, id, localizedName, localizedName.Capacity) != 0)
+ {
+ string lString = localizedName.ToString();
+ _ = FreeLibrary(hMod);
+ return lString;
+ }
+
+ _ = FreeLibrary(hMod);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// This method returns the localized path to a shell item (folder or file)
+ ///
+ /// The path to localize
+ /// The localized path or the original path if localized version is not available
+ public static string GetLocalizedPath(string path)
+ {
+ path = Environment.ExpandEnvironmentVariables(path);
+ string ext = Path.GetExtension(path);
+ var pathParts = path.Split("\\");
+ string[] locPath = new string[pathParts.Length];
+
+ for (int i = 0; i < pathParts.Length; i++)
+ {
+ int iElements = i + 1;
+ string lName = GetLocalizedName(string.Join("\\", pathParts[..iElements]));
+ locPath[i] = !string.IsNullOrEmpty(lName) ? lName : pathParts[i];
+ }
+
+ string newPath = string.Join("\\", locPath);
+ newPath = !newPath.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase) ? newPath + ext : newPath;
+
+ return newPath;
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
index b1ec70068..2f84fdbf5 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
@@ -249,24 +249,24 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable CurrentUserPackages()
{
- var u = WindowsIdentity.GetCurrent().User;
+ var user = WindowsIdentity.GetCurrent().User;
- if (u != null)
+ if (user != null)
{
- var id = u.Value;
- PackageManager m;
+ var userId = user.Value;
+ PackageManager packageManager;
try
{
- m = new PackageManager();
+ packageManager = new PackageManager();
}
catch
{
// Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0.
// Only happens on the first time, so a try catch can fix it.
- m = new PackageManager();
+ packageManager = new PackageManager();
}
- var ps = m.FindPackagesForUser(id);
- ps = ps.Where(p =>
+ var packages = packageManager.FindPackagesForUser(userId);
+ packages = packages.Where(p =>
{
try
{
@@ -277,12 +277,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
catch (Exception e)
{
- ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{id}", "An unexpected error occurred and "
+ ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", "An unexpected error occurred and "
+ $"unable to verify if package is valid", e);
return false;
}
});
- return ps;
+ return packages;
}
else
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index 3373948a3..dec43530e 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
public class Win32 : IProgram, IEquatable
{
public string Name { get; set; }
- public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
+ public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
public string IcoPath { get; set; }
///
/// Path of the file. It's the path of .lnk and .url for .lnk and .url files.
@@ -34,16 +34,22 @@ namespace Flow.Launcher.Plugin.Program.Programs
///
public string LnkResolvedPath { get; set; }
///
- /// Path of the actual executable file.
+ /// Path of the actual executable file. Args are included.
///
public string ExecutablePath => LnkResolvedPath ?? FullPath;
public string ParentDirectory { get; set; }
+ ///
+ /// Name of the executable for .lnk files
+ ///
public string ExecutableName { get; set; }
public string Description { get; set; }
public bool Valid { get; set; }
public bool Enabled { get; set; }
public string Location => ParentDirectory;
+ // Localized name based on windows display language
+ public string LocalizedName { get; set; } = string.Empty;
+
private const string ShortcutExtension = "lnk";
private const string UrlExtension = "url";
private const string ExeExtension = "exe";
@@ -63,48 +69,90 @@ namespace Flow.Launcher.Plugin.Program.Programs
Enabled = false
};
+ private static MatchResult Match(string query, IReadOnlyCollection candidates)
+ {
+ if (candidates.Count == 0)
+ return null;
+
+ var match = candidates.Select(candidate => StringMatcher.FuzzySearch(query, candidate))
+ .MaxBy(match => match.Score);
+
+ return match?.IsSearchPrecisionScoreMet() ?? false ? match : null;
+ }
public Result Result(string query, IPublicAPI api)
{
string title;
MatchResult matchResult;
- // We suppose Name won't be null
- if (!Main._settings.EnableDescription || Description == null || Name.StartsWith(Description))
+ // Name of the result
+ // Check equality to avoid matching again in candidates
+ bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName);
+ string resultName = useLocalizedName ? LocalizedName : Name;
+
+ if (!Main._settings.EnableDescription)
{
- title = Name;
- matchResult = StringMatcher.FuzzySearch(query, title);
- }
- else if (Description.StartsWith(Name))
- {
- title = Description;
- matchResult = StringMatcher.FuzzySearch(query, Description);
+ title = resultName;
+ matchResult = StringMatcher.FuzzySearch(query, resultName);
}
else
{
- title = $"{Name}: {Description}";
- var nameMatch = StringMatcher.FuzzySearch(query, Name);
- var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
- if (descriptionMatch.Score > nameMatch.Score)
+ if (string.IsNullOrEmpty(Description) || resultName.StartsWith(Description))
{
- for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
- {
- descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
- }
- matchResult = descriptionMatch;
+ // Description is invalid or included in resultName
+ // Description is always localized, so Name.StartsWith(Description) is generally useless
+ title = resultName;
+ matchResult = StringMatcher.FuzzySearch(query, resultName);
+ }
+ else if (Description.StartsWith(resultName))
+ {
+ // resultName included in Description
+ title = Description;
+ matchResult = StringMatcher.FuzzySearch(query, Description);
+ }
+ else
+ {
+ // Search in both
+ title = $"{resultName}: {Description}";
+ var nameMatch = StringMatcher.FuzzySearch(query, resultName);
+ var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
+ if (descriptionMatch.Score > nameMatch.Score)
+ {
+ for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
+ {
+ descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": "
+ }
+ matchResult = descriptionMatch;
+ }
+ else
+ {
+ matchResult = nameMatch;
+ }
}
- else matchResult = nameMatch;
}
+ List candidates = new List();
+
if (!matchResult.IsSearchPrecisionScoreMet())
{
if (ExecutableName != null) // only lnk program will need this one
- matchResult = StringMatcher.FuzzySearch(query, ExecutableName);
-
- if (!matchResult.IsSearchPrecisionScoreMet())
+ {
+ candidates.Add(ExecutableName);
+ }
+ if (useLocalizedName)
+ {
+ candidates.Add(Name);
+ }
+ matchResult = Match(query, candidates);
+ if (matchResult == null)
+ {
return null;
-
- matchResult.MatchData = new List();
+ }
+ else
+ {
+ // Nothing to highlight in title in this case
+ matchResult.MatchData.Clear();
+ }
}
string subtitle = string.Empty;
@@ -267,51 +315,40 @@ namespace Flow.Launcher.Plugin.Program.Programs
ShellLinkHelper _helper = new ShellLinkHelper();
string target = _helper.retrieveTargetPath(path);
- if (!string.IsNullOrEmpty(target))
+ if (!string.IsNullOrEmpty(target) && File.Exists(target))
{
- var extension = Extension(target);
- if (extension == ExeExtension && File.Exists(target))
+ program.LnkResolvedPath = Path.GetFullPath(target);
+ program.ExecutableName = Path.GetFileName(target);
+
+ var args = _helper.arguments;
+ if (!string.IsNullOrEmpty(args))
{
- program.LnkResolvedPath = Path.GetFullPath(target);
- program.ExecutableName = Path.GetFileName(target);
+ program.LnkResolvedPath += " " + args;
+ }
- var args = _helper.arguments;
- if(!string.IsNullOrEmpty(args))
+ var description = _helper.description;
+ if (!string.IsNullOrEmpty(description))
+ {
+ program.Description = description;
+ }
+ else
+ {
+ var info = FileVersionInfo.GetVersionInfo(target);
+ if (!string.IsNullOrEmpty(info.FileDescription))
{
- program.LnkResolvedPath += " " + args;
- }
-
- var description = _helper.description;
- if (!string.IsNullOrEmpty(description))
- {
- program.Description = description;
- }
- else
- {
- var info = FileVersionInfo.GetVersionInfo(target);
- if (!string.IsNullOrEmpty(info.FileDescription))
- {
- program.Description = info.FileDescription;
- }
+ program.Description = info.FileDescription;
}
}
}
- 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);
+ program.LocalizedName = ShellLocalization.GetLocalizedName(path);
- return Default;
+ return program;
}
catch (FileNotFoundException e)
{
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
- "|An unexpected error occurred in the calling method LnkProgram", e);
+ "|An unexpected error occurred in the calling method LnkProgram", e);
return Default;
}
@@ -378,7 +415,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
catch (FileNotFoundException e)
{
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
- $"|File not found when trying to load the program from {path}", e);
+ $"|File not found when trying to load the program from {path}", e);
return Default;
}
@@ -398,8 +435,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{
- IgnoreInaccessible = true,
- RecurseSubdirectories = recursive
+ IgnoreInaccessible = true, RecurseSubdirectories = recursive
}).Where(x => suffixes.Contains(Extension(x)));
}
@@ -408,7 +444,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var extension = Path.GetExtension(path)?.ToLowerInvariant();
if (!string.IsNullOrEmpty(extension))
{
- return extension.Substring(1); // remove dot
+ return extension.Substring(1); // remove dot
}
else
{
@@ -420,7 +456,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
// Disabled custom sources are not in DisabledProgramSources
var paths = directories.AsParallel()
- .SelectMany(s => EnumerateProgramsInDir(s, suffixes));
+ .SelectMany(s => EnumerateProgramsInDir(s, suffixes));
// Remove disabled programs in DisabledProgramSources
var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols));
@@ -452,8 +488,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
var toFilter = paths.Where(x => commonParents.All(parent => !IsSubPathOf(x, parent)))
- .AsParallel()
- .SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
+ .AsParallel()
+ .SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
var programs = ExceptDisabledSource(toFilter.Distinct())
.Select(x => GetProgramFromPath(x, protocols));
@@ -483,7 +519,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
var programs = ExceptDisabledSource(toFilter)
- .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue
+ .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue
return programs;
}
@@ -537,7 +573,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
ExeExtension => ExeProgram(path),
UrlExtension => UrlProgram(path, protocols),
_ => Win32Program(path)
- }; ;
+ };
+ ;
}
public static IEnumerable ExceptDisabledSource(IEnumerable paths)
@@ -577,7 +614,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable ProgramsHasher(IEnumerable programs)
{
- // TODO: Unable to distinguish multiple lnks to the same executable but with different params
return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant())
.AsParallel()
.SelectMany(g =>
@@ -748,10 +784,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var rel = Path.GetRelativePath(basePath, subPath);
return rel != "."
- && rel != ".."
- && !rel.StartsWith("../")
- && !rel.StartsWith(@"..\")
- && !Path.IsPathRooted(rel);
+ && rel != ".."
+ && !rel.StartsWith("../")
+ && !rel.StartsWith(@"..\")
+ && !Path.IsPathRooted(rel);
}
private static List GetCommonParents(IEnumerable programSources)
@@ -766,7 +802,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
foreach (var source in group)
{
if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
- source != p))
+ source != p))
{
parents.Remove(source);
}