Merge pull request #1736 from VictoriousRaptor/LocalizedLnkName

Use localized name for shell link programs
This commit is contained in:
VictoriousRaptor 2023-01-04 14:04:13 +08:00 committed by GitHub
commit a8d4f5daf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 225 additions and 86 deletions

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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
{

View file

@ -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);
}