mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
513 lines
No EOL
18 KiB
C#
513 lines
No EOL
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Win32;
|
|
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
|
|
{
|
|
[Serializable]
|
|
public class Win32 : IProgram
|
|
{
|
|
public string Name { get; set; }
|
|
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; }
|
|
public bool Valid { get; set; }
|
|
public bool Enabled { get; set; }
|
|
public string Location => ParentDirectory;
|
|
|
|
private const string ShortcutExtension = "lnk";
|
|
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 = StringMatcher.FuzzySearch(query, Path.GetFileNameWithoutExtension(FullPath));
|
|
|
|
var score = new[] { nameMatchResult.Score, descriptionMatchResult.Score, pathMatchResult.Score }.Max();
|
|
|
|
if (score < (int)StringMatcher.Instance.UserSettingSearchPrecision)
|
|
return null;
|
|
|
|
// We suppose Name won't be null
|
|
if (score == nameMatchResult.Score && (Description == null || Name.StartsWith(Description)))
|
|
{
|
|
title = Name;
|
|
matchResult = nameMatchResult;
|
|
}
|
|
else if (score == descriptionMatchResult.Score && Description.StartsWith(Name))
|
|
{
|
|
title = Description;
|
|
|
|
for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++)
|
|
{
|
|
descriptionMatchResult.MatchData[i] += Name.Length + 2; // 2 is ": "
|
|
}
|
|
|
|
matchResult = descriptionMatchResult;
|
|
}
|
|
else
|
|
{
|
|
title = $"{Name}: {Description}";
|
|
matchResult = pathMatchResult;
|
|
}
|
|
|
|
var result = new Result
|
|
{
|
|
Title = title,
|
|
SubTitle = LnkResolvedPath ?? FullPath,
|
|
IcoPath = IcoPath,
|
|
Score = matchResult.Score,
|
|
TitleHighlightData = matchResult.MatchData,
|
|
ContextData = this,
|
|
Action = _ =>
|
|
{
|
|
var info = new ProcessStartInfo
|
|
{
|
|
FileName = LnkResolvedPath ?? FullPath,
|
|
WorkingDirectory = ParentDirectory,
|
|
UseShellExecute = true
|
|
};
|
|
|
|
Main.StartProcess(Process.Start, info);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
public List<Result> ContextMenus(IPublicAPI api)
|
|
{
|
|
var contextMenus = new List<Result>
|
|
{
|
|
new Result
|
|
{
|
|
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_different_user"),
|
|
Action = _ =>
|
|
{
|
|
var info = new ProcessStartInfo
|
|
{
|
|
FileName = FullPath,
|
|
WorkingDirectory = ParentDirectory,
|
|
UseShellExecute = true
|
|
};
|
|
|
|
Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info));
|
|
|
|
return true;
|
|
},
|
|
IcoPath = "Images/user.png"
|
|
},
|
|
new Result
|
|
{
|
|
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
|
|
Action = _ =>
|
|
{
|
|
var info = new ProcessStartInfo
|
|
{
|
|
FileName = FullPath,
|
|
WorkingDirectory = ParentDirectory,
|
|
Verb = "runas",
|
|
UseShellExecute = true
|
|
};
|
|
|
|
Task.Run(() => Main.StartProcess(Process.Start, info));
|
|
|
|
return true;
|
|
},
|
|
IcoPath = "Images/cmd.png"
|
|
},
|
|
new Result
|
|
{
|
|
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
|
|
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.StartProcess(Process.Start,
|
|
new ProcessStartInfo(
|
|
!string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer)
|
|
? Main._settings.CustomizedExplorer
|
|
: Settings.Explorer,
|
|
args));
|
|
|
|
return true;
|
|
},
|
|
IcoPath = "Images/folder.png"
|
|
}
|
|
};
|
|
return contextMenus;
|
|
}
|
|
|
|
|
|
public override string ToString()
|
|
{
|
|
return ExecutableName;
|
|
}
|
|
|
|
private static Win32 Win32Program(string path)
|
|
{
|
|
try
|
|
{
|
|
var p = new Win32
|
|
{
|
|
Name = Path.GetFileNameWithoutExtension(path),
|
|
IcoPath = path,
|
|
FullPath = path,
|
|
UniqueIdentifier = path,
|
|
ParentDirectory = Directory.GetParent(path).FullName,
|
|
Description = string.Empty,
|
|
Valid = true,
|
|
Enabled = true
|
|
};
|
|
return p;
|
|
}
|
|
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);
|
|
|
|
return new Win32() {Valid = false, Enabled = false};
|
|
}
|
|
}
|
|
|
|
private static Win32 LnkProgram(string path)
|
|
{
|
|
var program = Win32Program(path);
|
|
try
|
|
{
|
|
const int MAX_PATH = 260;
|
|
StringBuilder buffer = new StringBuilder(MAX_PATH);
|
|
ShellLinkHelper _helper = new ShellLinkHelper();
|
|
string target = _helper.retrieveTargetPath(path);
|
|
|
|
if (!string.IsNullOrEmpty(target))
|
|
{
|
|
var extension = Extension(target);
|
|
if (extension == ExeExtension && File.Exists(target))
|
|
{
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
var info = FileVersionInfo.GetVersionInfo(target);
|
|
if (!string.IsNullOrEmpty(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.Valid = false;
|
|
return program;
|
|
}
|
|
#if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
|
|
catch (Exception e)
|
|
{
|
|
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
|
|
"|An unexpected error occurred in the calling method LnkProgram", e);
|
|
|
|
program.Valid = false;
|
|
return program;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private static Win32 ExeProgram(string path)
|
|
{
|
|
try
|
|
{
|
|
var program = Win32Program(path);
|
|
var info = FileVersionInfo.GetVersionInfo(path);
|
|
if (!string.IsNullOrEmpty(info.FileDescription))
|
|
program.Description = info.FileDescription;
|
|
return program;
|
|
}
|
|
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);
|
|
|
|
return new Win32() {Valid = false, Enabled = false};
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes)
|
|
{
|
|
if (!Directory.Exists(directory))
|
|
return Enumerable.Empty<string>();
|
|
|
|
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
|
|
{
|
|
IgnoreInaccessible = true,
|
|
RecurseSubdirectories = true
|
|
}).Where(x => suffixes.Contains(Extension(x)));
|
|
}
|
|
|
|
private static string Extension(string path)
|
|
{
|
|
var extension = Path.GetExtension(path)?.ToLower();
|
|
if (!string.IsNullOrEmpty(extension))
|
|
{
|
|
return extension.Substring(1);
|
|
}
|
|
else
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes)
|
|
{
|
|
var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
|
|
.SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x)
|
|
.Distinct();
|
|
|
|
var programs = paths.Select(x => Extension(x) switch
|
|
{
|
|
ExeExtension => ExeProgram(x),
|
|
ShortcutExtension => LnkProgram(x),
|
|
_ => Win32Program(x)
|
|
});
|
|
|
|
|
|
return programs;
|
|
}
|
|
|
|
private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes)
|
|
{
|
|
var disabledProgramsList = Main._settings.DisabledProgramSources;
|
|
|
|
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
|
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
|
|
var paths1 = ProgramPaths(directory1, suffixes);
|
|
var paths2 = ProgramPaths(directory2, suffixes);
|
|
|
|
var toFilter = paths1.Concat(paths2);
|
|
|
|
var programs = ExceptDisabledSource(toFilter.Distinct())
|
|
.Select(x => Extension(x) switch
|
|
{
|
|
ShortcutExtension => LnkProgram(x),
|
|
_ => Win32Program(x)
|
|
}).Where(x => x.Valid);
|
|
return programs;
|
|
}
|
|
|
|
private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes)
|
|
{
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
|
|
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
|
|
|
|
IEnumerable<string> toFilter = Enumerable.Empty<string>();
|
|
|
|
using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths);
|
|
using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths);
|
|
|
|
if (rootMachine != null)
|
|
{
|
|
toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine));
|
|
}
|
|
|
|
if (rootUser != null)
|
|
{
|
|
toFilter = toFilter.Concat(GetPathFromRegistry(rootUser));
|
|
}
|
|
|
|
|
|
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
|
|
|
|
var filtered = ExceptDisabledSource(toFilter);
|
|
|
|
return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue
|
|
}
|
|
|
|
private static IEnumerable<string> GetPathFromRegistry(RegistryKey root)
|
|
{
|
|
return root
|
|
.GetSubKeyNames()
|
|
.Select(x => GetProgramPathFromRegistrySubKeys(root, x))
|
|
.Distinct();
|
|
}
|
|
|
|
private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey)
|
|
{
|
|
var path = string.Empty;
|
|
try
|
|
{
|
|
using (var key = root.OpenSubKey(subkey))
|
|
{
|
|
if (key == null)
|
|
return string.Empty;
|
|
|
|
var defaultValue = string.Empty;
|
|
path = key.GetValue(defaultValue) as string;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
return string.Empty;
|
|
|
|
// fix path like this: ""\"C:\\folder\\executable.exe\""
|
|
return path = path.Trim('"', ' ');
|
|
}
|
|
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);
|
|
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private static Win32 GetProgramFromPath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return null;
|
|
|
|
path = Environment.ExpandEnvironmentVariables(path);
|
|
|
|
if (!File.Exists(path))
|
|
return null;
|
|
|
|
var entry = Win32Program(path);
|
|
entry.ExecutableName = Path.GetFileName(path);
|
|
|
|
return entry;
|
|
}
|
|
|
|
public static IEnumerable<string> ExceptDisabledSource(IEnumerable<string> sources)
|
|
{
|
|
return ExceptDisabledSource(sources, x => x);
|
|
}
|
|
|
|
public static IEnumerable<TSource> ExceptDisabledSource<TSource>(IEnumerable<TSource> sources,
|
|
Func<TSource, string> keySelector)
|
|
{
|
|
return Main._settings.DisabledProgramSources.Count == 0
|
|
? sources
|
|
: ExceptDisabledSourceEnumerable(sources, keySelector);
|
|
|
|
static IEnumerable<TSource> ExceptDisabledSourceEnumerable(IEnumerable<TSource> elements,
|
|
Func<TSource, string> 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<T> DistinctBy<T, R>(IEnumerable<T> source, Func<T,R> selector)
|
|
{
|
|
var set = new HashSet<R>();
|
|
foreach (var item in source)
|
|
{
|
|
if (set.Add(selector(item)))
|
|
yield return item;
|
|
}
|
|
}
|
|
|
|
private static Win32[] ProgramsHasher(IEnumerable<Win32> 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 = Enumerable.Empty<Win32>();
|
|
|
|
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
|
|
programs = programs.Concat(unregistered);
|
|
|
|
if (settings.EnableRegistrySource)
|
|
{
|
|
var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
|
|
programs = programs.Concat(appPaths);
|
|
}
|
|
|
|
if (settings.EnableStartMenuSource)
|
|
{
|
|
var startMenu = StartMenuPrograms(settings.ProgramSuffixes);
|
|
programs = programs.Concat(startMenu);
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
throw e;
|
|
}
|
|
#endif
|
|
|
|
#if !DEBUG //Only do a catch all in production.
|
|
catch (Exception e)
|
|
{
|
|
ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e);
|
|
|
|
return Array.Empty<Win32>();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
} |