mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #1736 from VictoriousRaptor/LocalizedLnkName
Use localized name for shell link programs
This commit is contained in:
commit
a8d4f5daf7
4 changed files with 225 additions and 86 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// <summary>
|
||||
/// 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 <see cref="GetLocalizedName"/>
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the localized name of a shell item.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the shell item (e. g. shortcut 'File Explorer.lnk').</param>
|
||||
/// <returns>The localized name as string or <see cref="string.Empty"/>.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method returns the localized path to a shell item (folder or file)
|
||||
/// </summary>
|
||||
/// <param name="path">The path to localize</param>
|
||||
/// <returns>The localized path or the original path if localized version is not available</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -249,24 +249,24 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
private static IEnumerable<Package> 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
public class Win32 : IProgram, IEquatable<Win32>
|
||||
{
|
||||
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; }
|
||||
/// <summary>
|
||||
/// 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
|
|||
/// </summary>
|
||||
public string LnkResolvedPath { get; set; }
|
||||
/// <summary>
|
||||
/// Path of the actual executable file.
|
||||
/// Path of the actual executable file. Args are included.
|
||||
/// </summary>
|
||||
public string ExecutablePath => LnkResolvedPath ?? FullPath;
|
||||
public string ParentDirectory { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the executable for .lnk files
|
||||
/// </summary>
|
||||
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<string> 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<string> candidates = new List<string>();
|
||||
|
||||
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<int>();
|
||||
}
|
||||
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<string> ExceptDisabledSource(IEnumerable<string> paths)
|
||||
|
|
@ -577,7 +614,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
|
||||
private static IEnumerable<Win32> ProgramsHasher(IEnumerable<Win32> 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<string> GetCommonParents(IEnumerable<ProgramSource> 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue