Merge branch 'dev' into DotNet5Upgrade

This commit is contained in:
Jeremy Wu 2021-02-16 07:46:20 +11:00
commit ab4e3f87cb
36 changed files with 793 additions and 308 deletions

View file

@ -53,6 +53,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Droplex" Version="1.2.0" />
<PackageReference Include="FSharp.Core" Version="4.7.1" /> <PackageReference Include="FSharp.Core" Version="4.7.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" /> <PackageReference Include="squirrel.windows" Version="1.5.2" />
</ItemGroup> </ItemGroup>

View file

@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Droplex;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -22,7 +23,7 @@ namespace Flow.Launcher.Core.Plugin
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings) public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
{ {
var dotnetPlugins = DotNetPlugins(metadatas); var dotnetPlugins = DotNetPlugins(metadatas);
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory); var pythonPlugins = PythonPlugins(metadatas, settings);
var executablePlugins = ExecutablePlugins(metadatas); var executablePlugins = ExecutablePlugins(metadatas);
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList(); var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
return plugins; return plugins;
@ -113,11 +114,14 @@ namespace Flow.Launcher.Core.Plugin
return plugins; return plugins;
} }
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirectory) public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
{ {
// try to set Constant.PythonPath, either from if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>();
// Try setting Constant.PythonPath first, either from
// PATH or from the given pythonDirectory // PATH or from the given pythonDirectory
if (string.IsNullOrEmpty(pythonDirectory)) if (string.IsNullOrEmpty(settings.PythonDirectory))
{ {
var paths = Environment.GetEnvironmentVariable(PATH); var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null) if (paths != null)
@ -129,47 +133,103 @@ namespace Flow.Launcher.Core.Plugin
if (pythonInPath) if (pythonInPath)
{ {
Constant.PythonPath = PythonExecutable; Constant.PythonPath =
Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable);
settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath);
} }
else else
{ {
Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH."); Log.Error("PluginsLoader","Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
} }
} }
else
{
Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set.");
}
} }
else else
{ {
var path = Path.Combine(pythonDirectory, PythonExecutable); var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
if (File.Exists(path)) if (File.Exists(path))
{ {
Constant.PythonPath = path; Constant.PythonPath = path;
} }
else else
{ {
Log.Error($"|PluginsLoader.PythonPlugins|Can't find python executable in {path}"); Log.Error("PluginsLoader",$"Tried to automatically set from Settings.PythonDirectory " +
$"but can't find python executable in {path}", "PythonPlugins");
} }
} }
// if we have a path to the python executable, if (string.IsNullOrEmpty(settings.PythonDirectory))
// load every python plugin pair.
if (String.IsNullOrEmpty(Constant.PythonPath))
{ {
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
"would you like to install Python to run them? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
var pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
DroplexPackage.Drop(App.python3_9_1).Wait();
var installedPythonDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39");
var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory))
{
MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
"PythonPlugins");
return new List<PluginPair>(); return new List<PluginPair>();
} }
else
{ return source
return source .Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python) .Select(metadata => new PluginPair
.Select(metadata => new PluginPair {
{ Plugin = new PythonPlugin(Constant.PythonPath),
Plugin = new PythonPlugin(Constant.PythonPath), Metadata = metadata
Metadata = metadata })
}); .ToList();
}
} }
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source) public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)

View file

@ -75,7 +75,7 @@ namespace Flow.Launcher.Infrastructure.Http
{ {
try try
{ {
using var response = await client.GetAsync(url, token); using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (response.StatusCode == HttpStatusCode.OK) if (response.StatusCode == HttpStatusCode.OK)
{ {
await using var fileStream = new FileStream(filePath, FileMode.CreateNew); await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
@ -135,7 +135,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default) public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
{ {
Log.Debug($"|Http.Get|Url <{url}>"); Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url, token); var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
return await response.Content.ReadAsStreamAsync(); return await response.Content.ReadAsStreamAsync();
} }
} }

View file

@ -12,7 +12,7 @@ namespace Flow.Launcher.Infrastructure.Storage
public class JsonStrorage<T> where T : new() public class JsonStrorage<T> where T : new()
{ {
private readonly JsonSerializerOptions _serializerSettings; private readonly JsonSerializerOptions _serializerSettings;
private T _data; protected T _data;
// need a new directory name // need a new directory name
public const string DirectoryName = "Settings"; public const string DirectoryName = "Settings";
public const string FileSuffix = ".json"; public const string FileSuffix = ".json";

View file

@ -15,5 +15,10 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
} }
public PluginJsonStorage(T data) : this()
{
_data = data;
}
} }
} }

View file

@ -3,6 +3,7 @@ using JetBrains.Annotations;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -88,17 +89,86 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
/// <summary>
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
/// </summary>
/// <param name="query">Query string</param>
/// <param name="stringToCompare">The string that will be compared against the query</param>
/// <returns>Match results</returns>
MatchResult FuzzySearch(string query, string stringToCompare); MatchResult FuzzySearch(string query, string stringToCompare);
/// <summary>
/// Http download the spefic url and return as string
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
/// <returns>Task to get string result</returns>
Task<string> HttpGetStringAsync(string url, CancellationToken token = default); Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
/// <summary>
/// Http download the spefic url and return as stream
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
/// <returns>Task to get stream result</returns>
Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default); Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default);
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath); /// <summary>
/// Download the specific url to a cretain file path
/// </summary>
/// <param name="url">URL to download file</param>
/// <param name="token">place to store file</param>
/// <returns>Task showing the progress</returns>
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
/// <summary>
/// Add ActionKeyword for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
void AddActionKeyword(string pluginId, string newActionKeyword); void AddActionKeyword(string pluginId, string newActionKeyword);
/// <summary>
/// Remove ActionKeyword for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
void RemoveActionKeyword(string pluginId, string oldActionKeyword); void RemoveActionKeyword(string pluginId, string oldActionKeyword);
/// <summary>
/// Log debug message
/// Message will only be logged in Debug mode
/// </summary>
void LogDebug(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log info message
/// </summary>
void LogInfo(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log warning message
/// </summary>
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// </summary>
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
/// <summary>
/// Load JsonStorage for current plugin. This is the method used to load settings from json in Flow
/// </summary>
/// <typeparam name="T">Type for deserialization</typeparam>
/// <returns></returns>
T LoadJsonStorage<T>() where T : new();
/// <summary>
/// Save JsonStorage for current plugin. This is the method used to save settings to json in Flow
/// </summary>
/// <typeparam name="T">Type for Serialization</typeparam>
/// <returns></returns>
void SaveJsonStorage<T>(T setting) where T : new();
} }
} }

View file

@ -1,4 +1,4 @@
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Plugin
{ {
/// <summary> /// <summary>
/// Save plugin settings/cache, /// Save plugin settings/cache,

View file

@ -66,6 +66,9 @@
<Content Include="Images\*.svg"> <Content Include="Images\*.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\*.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -121,6 +121,7 @@
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
<system:String x:Key="success">Success</system:String> <system:String x:Key="success">Success</system:String>
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String> <system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>
<!--Custom Query Hotkey Dialog--> <!--Custom Query Hotkey Dialog-->

View file

@ -34,6 +34,7 @@
<Window.InputBindings> <Window.InputBindings>
<KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding> <KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding>
<KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding> <KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding>
<KeyBinding Key="F5" Command="{Binding ReloadPluginDataCommand}"></KeyBinding>
<KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding> <KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding>
<KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding> <KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding>
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding> <KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding>

View file

@ -66,7 +66,7 @@ namespace Flow.Launcher
} }
if (!File.Exists(iconPath)) if (!File.Exists(iconPath))
{ {
imgIco.Source = ImageLoader.Load(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\app.png")); imgIco.Source = ImageLoader.Load(Path.Combine(Constant.ProgramDirectory, "Images\\app.png"));
} }
else { else {
imgIco.Source = ImageLoader.Load(iconPath); imgIco.Source = ImageLoader.Load(iconPath);

View file

@ -18,6 +18,9 @@ using System.Threading;
using System.IO; using System.IO;
using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Http;
using JetBrains.Annotations; using JetBrains.Annotations;
using System.Runtime.CompilerServices;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -61,18 +64,15 @@ namespace Flow.Launcher
// which will cause ungraceful exit // which will cause ungraceful exit
SaveAppAllSettings(); SaveAppAllSettings();
// Restart requires Squirrel's Update.exe to be present in the parent folder,
// it is only published from the project's release pipeline. When debugging without it,
// the project may not restart or just terminates. This is expected.
UpdateManager.RestartApp(Constant.ApplicationFileName); UpdateManager.RestartApp(Constant.ApplicationFileName);
} }
public void RestarApp() public void RestarApp() => RestartApp();
{
RestartApp();
}
public void CheckForNewUpdate() public void CheckForNewUpdate() => _settingsVM.UpdateApp();
{
_settingsVM.UpdateApp();
}
public void SaveAppAllSettings() public void SaveAppAllSettings()
{ {
@ -82,15 +82,9 @@ namespace Flow.Launcher
ImageLoader.Save(); ImageLoader.Save();
} }
public Task ReloadAllPluginData() public Task ReloadAllPluginData() => PluginManager.ReloadData();
{
return PluginManager.ReloadData();
}
public void ShowMsg(string title, string subTitle = "", string iconPath = "") public void ShowMsg(string title, string subTitle = "", string iconPath = "") => ShowMsg(title, subTitle, iconPath, true);
{
ShowMsg(title, subTitle, iconPath, true);
}
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
{ {
@ -109,54 +103,40 @@ namespace Flow.Launcher
}); });
} }
public void StartLoadingBar() public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible;
{
_mainVM.ProgressBarVisibility = Visibility.Visible;
}
public void StopLoadingBar() public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
{
_mainVM.ProgressBarVisibility = Visibility.Collapsed;
}
public string GetTranslation(string key) public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key);
{
return InternationalizationManager.Instance.GetTranslation(key);
}
public List<PluginPair> GetAllPlugins() public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
{
return PluginManager.AllPlugins.ToList();
}
public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare);
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url);
{
return Http.GetAsync(url);
}
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) => Http.GetStreamAsync(url);
{
return Http.GetStreamAsync(url);
}
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath) public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) => Http.DownloadAsync(url, filePath, token);
{
return Http.DownloadAsync(url, filePath);
}
public void AddActionKeyword(string pluginId, string newActionKeyword) public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword);
{
PluginManager.AddActionKeyword(pluginId, newActionKeyword); public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
}
public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName);
public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName);
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName);
public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);
public T LoadJsonStorage<T>() where T : new() => new PluginJsonStorage<T>().Load();
public void SaveJsonStorage<T>(T setting) where T : new() => new PluginJsonStorage<T>(setting).Save();
public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
{
PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
}
#endregion #endregion
#region Private Methods #region Private Methods

View file

@ -11,7 +11,7 @@
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
x:Class="Flow.Launcher.SettingWindow" x:Class="Flow.Launcher.SettingWindow"
mc:Ignorable="d" mc:Ignorable="d"
Icon="Images\app.png" Icon="Images\app.ico"
Title="{DynamicResource flowlauncher_settings}" Title="{DynamicResource flowlauncher_settings}"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"

View file

@ -19,6 +19,7 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage; using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using System.Threading.Channels;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
@ -46,7 +47,7 @@ namespace Flow.Launcher.ViewModel
private readonly Internationalization _translator = InternationalizationManager.Instance; private readonly Internationalization _translator = InternationalizationManager.Instance;
private BufferBlock<ResultsForUpdate> _resultsUpdateQueue; private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
private Task _resultsViewUpdateTask; private Task _resultsViewUpdateTask;
#endregion #endregion
@ -85,29 +86,32 @@ namespace Flow.Launcher.ViewModel
private void RegisterViewUpdate() private void RegisterViewUpdate()
{ {
_resultsUpdateQueue = new BufferBlock<ResultsForUpdate>(); var resultUpdateChannel = Channel.CreateUnbounded<ResultsForUpdate>();
_resultsUpdateChannelWriter = resultUpdateChannel.Writer;
_resultsViewUpdateTask = _resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
async Task updateAction() async Task updateAction()
{ {
var queue = new Dictionary<string, ResultsForUpdate>(); var queue = new Dictionary<string, ResultsForUpdate>();
while (await _resultsUpdateQueue.OutputAvailableAsync()) var channelReader = resultUpdateChannel.Reader;
// it is not supposed to be false because it won't be complete
while (await channelReader.WaitToReadAsync())
{ {
queue.Clear();
await Task.Delay(20); await Task.Delay(20);
while (_resultsUpdateQueue.TryReceive(out var item)) while (channelReader.TryRead(out var item))
{ {
if (!item.Token.IsCancellationRequested) if (!item.Token.IsCancellationRequested)
queue[item.ID] = item; queue[item.ID] = item;
} }
UpdateResultView(queue.Values); UpdateResultView(queue.Values);
queue.Clear();
} }
}
; Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends");
};
void continueAction(Task t) void continueAction(Task t)
{ {
@ -115,8 +119,8 @@ namespace Flow.Launcher.ViewModel
throw t.Exception; throw t.Exception;
#else #else
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
_resultsViewUpdateTask = _resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
#endif #endif
} }
} }
@ -131,7 +135,10 @@ namespace Flow.Launcher.ViewModel
if (e.Query.RawQuery == QueryText) // TODO: allow cancellation if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
{ {
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
_resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
};
} }
}; };
} }
@ -225,6 +232,25 @@ namespace Flow.Launcher.ViewModel
SelectedResults = Results; SelectedResults = Results;
} }
}); });
ReloadPluginDataCommand = new RelayCommand(_ =>
{
var msg = new Msg { Owner = Application.Current.MainWindow };
MainWindowVisibility = Visibility.Collapsed;
PluginManager
.ReloadData()
.ContinueWith(_ =>
Application.Current.Dispatcher.Invoke(() =>
{
msg.Show(
InternationalizationManager.Instance.GetTranslation("success"),
InternationalizationManager.Instance.GetTranslation("completedSuccessfully"),
"");
}))
.ConfigureAwait(false);
});
} }
#endregion #endregion
@ -313,6 +339,7 @@ namespace Flow.Launcher.ViewModel
public ICommand LoadContextMenuCommand { get; set; } public ICommand LoadContextMenuCommand { get; set; }
public ICommand LoadHistoryCommand { get; set; } public ICommand LoadHistoryCommand { get; set; }
public ICommand OpenResultCommand { get; set; } public ICommand OpenResultCommand { get; set; }
public ICommand ReloadPluginDataCommand { get; set; }
public string OpenResultCommandModifiers { get; private set; } public string OpenResultCommandModifiers { get; private set; }
@ -512,9 +539,12 @@ namespace Flow.Launcher.ViewModel
await Task.Yield(); await Task.Yield();
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
if (!currentCancellationToken.IsCancellationRequested && results != null) if (currentCancellationToken.IsCancellationRequested || results == null) return;
_resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query,
currentCancellationToken)); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
};
} }
}, currentCancellationToken) }, currentCancellationToken)
.ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), .ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception),

View file

@ -6,7 +6,7 @@ using System.Threading;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public class ResultsForUpdate public struct ResultsForUpdate
{ {
public List<Result> Results { get; } public List<Result> Results { get; }
@ -16,13 +16,6 @@ namespace Flow.Launcher.ViewModel
public Query Query { get; } public Query Query { get; }
public CancellationToken Token { get; } public CancellationToken Token { get; }
public ResultsForUpdate(List<Result> results, string resultID, CancellationToken token)
{
Results = results;
ID = resultID;
Token = token;
}
public ResultsForUpdate(List<Result> results, PluginMetadata metadata, Query query, CancellationToken token) public ResultsForUpdate(List<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
{ {
Results = results; Results = results;

View file

@ -183,13 +183,12 @@ namespace Flow.Launcher.ViewModel
private List<ResultViewModel> NewResults(List<Result> newRawResults, string resultId) private List<ResultViewModel> NewResults(List<Result> newRawResults, string resultId)
{ {
if (newRawResults.Count == 0) if (newRawResults.Count == 0)
return Results.ToList(); return Results;
var results = Results as IEnumerable<ResultViewModel>;
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
return results.Where(r => r.Result.PluginID != resultId) return Results.Where(r => r.Result.PluginID != resultId)
.Concat(newResults) .Concat(newResults)
.OrderByDescending(r => r.Result.Score) .OrderByDescending(r => r.Result.Score)
.ToList(); .ToList();
@ -198,11 +197,9 @@ namespace Flow.Launcher.ViewModel
private List<ResultViewModel> NewResults(IEnumerable<ResultsForUpdate> resultsForUpdates) private List<ResultViewModel> NewResults(IEnumerable<ResultsForUpdate> resultsForUpdates)
{ {
if (!resultsForUpdates.Any()) if (!resultsForUpdates.Any())
return Results.ToList(); return Results;
var results = Results as IEnumerable<ResultViewModel>; return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID))
return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID))
.Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings)))
.OrderByDescending(rv => rv.Result.Score) .OrderByDescending(rv => rv.Result.Score)
.ToList(); .ToList();

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -12,4 +12,6 @@
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_newTab">New tab</system:String> <system:String x:Key="flowlauncher_plugin_browserbookmark_settings_newTab">New tab</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_setBrowserFromPath">Set browser from path:</system:String> <system:String x:Key="flowlauncher_plugin_browserbookmark_settings_setBrowserFromPath">Set browser from path:</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_choose">Choose</system:String> <system:String x:Key="flowlauncher_plugin_browserbookmark_settings_choose">Choose</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_copyurl_title">Copy url</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_copyurl_subtitle">Copy the bookmark's url to clipboard</system:String>
</ResourceDictionary> </ResourceDictionary>

View file

@ -1,6 +1,9 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
@ -9,7 +12,7 @@ using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark
{ {
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, ISavable, IContextMenu
{ {
private PluginInitContext context; private PluginInitContext context;
@ -60,7 +63,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
} }
return true; return true;
} },
ContextData = new BookmarkAttributes { Url = c.Url }
}).Where(r => r.Score > 0); }).Where(r => r.Score > 0);
return returnList.ToList(); return returnList.ToList();
} }
@ -84,7 +88,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
} }
return true; return true;
} },
ContextData = new BookmarkAttributes { Url = c.Url }
}).ToList(); }).ToList();
} }
} }
@ -115,5 +120,39 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
{ {
_storage.Save(); _storage.Save();
} }
public List<Result> LoadContextMenus(Result selectedResult)
{
return new List<Result>() {
new Result
{
Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
SubTitle = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
Action = _ =>
{
try
{
Clipboard.SetDataObject(((BookmarkAttributes)selectedResult.ContextData).Url);
return true;
}
catch (Exception e)
{
var message = "Failed to set url in clipboard";
Log.Exception("Main",message, e, "LoadContextMenus");
context.API.ShowMsg(message);
return false;
}
},
IcoPath = "Images\\copylink.png"
}};
}
internal class BookmarkAttributes
{
internal string Url { get; set; }
}
} }
} }

View file

@ -4,7 +4,7 @@
"Name": "Browser Bookmarks", "Name": "Browser Bookmarks",
"Description": "Search your browser bookmarks", "Description": "Search your browser bookmarks",
"Author": "qianlifeng, Ioannis G.", "Author": "qianlifeng, Ioannis G.",
"Version": "1.3.2", "Version": "1.4.0",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll", "ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",

View file

@ -18,6 +18,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup> </ItemGroup>

View file

@ -37,20 +37,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
Settings = viewModel.Settings; Settings = viewModel.Settings;
contextMenu = new ContextMenu(Context); contextMenu = new ContextMenu(Context);
pluginManager = new PluginsManager(Context, Settings); pluginManager = new PluginsManager(Context, Settings);
var updateManifestTask = pluginManager.UpdateManifest(); _ = pluginManager.UpdateManifest().ContinueWith(_ =>
_ = updateManifestTask.ContinueWith(t => {
{ lastUpdateTime = DateTime.Now;
if (t.IsCompletedSuccessfully) }, TaskContinuationOptions.OnlyOnRanToCompletion);
{
lastUpdateTime = DateTime.Now;
}
else
{
context.API.ShowMsg("Plugin Manifest Download Fail.",
"Please check if you can connect to github.com. " +
"This error means you may not be able to Install and Update Plugin.", pluginManager.icoPath, false);
}
});
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -69,15 +59,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours
{ {
await pluginManager.UpdateManifest(); _ = pluginManager.UpdateManifest().ContinueWith(t =>
lastUpdateTime = DateTime.Now; {
lastUpdateTime = DateTime.Now;
}, TaskContinuationOptions.OnlyOnRanToCompletion);
} }
return search switch return search switch
{ {
var s when s.StartsWith(Settings.HotKeyInstall) => await pluginManager.RequestInstallOrUpdate(s, token), var s when s.StartsWith(Settings.HotKeyInstall) => await pluginManager.RequestInstallOrUpdate(s, token),
var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s), var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s),
var s when s.StartsWith(Settings.HotkeyUpdate) => pluginManager.RequestUpdate(s), var s when s.StartsWith(Settings.HotkeyUpdate) => await pluginManager.RequestUpdate(s, token),
_ => pluginManager.GetDefaultHotKeys().Where(hotkey => _ => pluginManager.GetDefaultHotKeys().Where(hotkey =>
{ {
hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score; hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score;

View file

@ -1,3 +1,4 @@
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
@ -47,9 +48,23 @@ namespace Flow.Launcher.Plugin.PluginsManager
Settings = settings; Settings = settings;
} }
internal async Task UpdateManifest() private Task _downloadManifestTask = Task.CompletedTask;
internal Task UpdateManifest()
{ {
await pluginsManifest.DownloadManifest(); if (_downloadManifestTask.Status == TaskStatus.Running)
{
return _downloadManifestTask;
}
else
{
return _downloadManifestTask = pluginsManifest.DownloadManifest().ContinueWith(t =>
Context.API.ShowMsg("Plugin Manifest Download Fail.",
"Please check if you can connect to github.com. " +
"This error means you may not be able to Install and Update Plugin.", icoPath, false),
TaskContinuationOptions.OnlyOnFaulted);
}
} }
internal List<Result> GetDefaultHotKeys() internal List<Result> GetDefaultHotKeys()
@ -150,8 +165,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.RestartApp(); Context.API.RestartApp();
} }
internal List<Result> RequestUpdate(string search) internal async ValueTask<List<Result>> RequestUpdate(string search, CancellationToken token)
{ {
if (!pluginsManifest.UserPlugins.Any())
{
await UpdateManifest();
}
token.ThrowIfCancellationRequested();
var autocompletedResults = AutoCompleteReturnAllResults(search, var autocompletedResults = AutoCompleteReturnAllResults(search,
Settings.HotkeyUpdate, Settings.HotkeyUpdate,
"Update", "Update",
@ -275,20 +297,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
.ToList(); .ToList();
} }
private Task _downloadManifestTask = Task.CompletedTask;
internal async ValueTask<List<Result>> RequestInstallOrUpdate(string searchName, CancellationToken token) internal async ValueTask<List<Result>> RequestInstallOrUpdate(string searchName, CancellationToken token)
{ {
if (!pluginsManifest.UserPlugins.Any() && if (!pluginsManifest.UserPlugins.Any())
_downloadManifestTask.Status != TaskStatus.Running)
{ {
_downloadManifestTask = pluginsManifest.DownloadManifest(); await UpdateManifest();
} }
await _downloadManifestTask; token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested)
return null;
var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim(); var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim();
@ -400,6 +416,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
private void Uninstall(PluginMetadata plugin) private void Uninstall(PluginMetadata plugin)
{ {
PluginManager.Settings.Plugins.Remove(plugin.ID);
PluginManager.AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
// Marked for deletion. Will be deleted on next start up // Marked for deletion. Will be deleted on next start up
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt")); using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
} }

View file

@ -6,7 +6,7 @@
"Name": "Plugins Manager", "Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Author": "Jeremy Wu", "Author": "Jeremy Wu",
"Version": "1.6.3", "Version": "1.7.0",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",

View file

@ -40,17 +40,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="AppxPackagingTlb">
<HintPath>.\AppxPackagingTlb.dll</HintPath>
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
<Reference Include="ShObjIdlTlb">
<HintPath>.\ShObjIdlTlb.dll</HintPath>
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Languages\*.xaml"> <Content Include="Languages\*.xaml">

View file

@ -228,7 +228,6 @@ namespace Flow.Launcher.Plugin.Program
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info) public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
{ {
bool hide;
try try
{ {
runProcess(info); runProcess(info);

View file

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

View file

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Windows.Storage;
namespace Flow.Launcher.Plugin.Program.Programs
{
public class AppxPackageHelper
{
// This function returns a list of attributes of applications
public List<IAppxManifestApplication> getAppsFromManifest(IStream stream)
{
List<IAppxManifestApplication> apps = new List<IAppxManifestApplication>();
var appxFactory = new AppxFactory();
var reader = ((IAppxFactory)appxFactory).CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
while (manifestApps.GetHasCurrent())
{
string appListEntry;
var manifestApp = manifestApps.GetCurrent();
manifestApp.GetStringValue("AppListEntry", out appListEntry);
if (appListEntry != "none")
{
apps.Add(manifestApp);
}
manifestApps.MoveNext();
}
return apps;
}
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport]
public class AppxFactory
{
}
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxFactory
{
void _VtblGap0_2(); // skip 2 methods
IAppxManifestReader CreateManifestReader(IStream inputStream);
}
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestReader
{
void _VtblGap0_1(); // skip 1 method
IAppxManifestProperties GetProperties();
void _VtblGap1_5(); // skip 5 methods
IAppxManifestApplicationsEnumerator GetApplications();
}
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplicationsEnumerator
{
IAppxManifestApplication GetCurrent();
bool GetHasCurrent();
bool MoveNext();
}
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplication
{
[PreserveSig]
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
[PreserveSig]
int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
}
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestProperties
{
[PreserveSig]
int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)]string name, out bool value);
[PreserveSig]
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
}
}
}

View file

@ -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
{
/// <summary>Retrieves the path and file name of a Shell link object</summary>
void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
void GetIDList(out IntPtr ppidl);
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
void SetIDList(IntPtr pidl);
/// <summary>Retrieves the description string for a Shell link object</summary>
void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
/// <summary>Sets the name of the working directory for a Shell link object</summary>
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
/// <summary>Sets the command-line arguments for a Shell link object</summary>
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
/// <summary>Retrieves the hot key for a Shell link object</summary>
void GetHotkey(out short pwHotkey);
/// <summary>Sets a hot key for a Shell link object</summary>
void SetHotkey(short wHotkey);
/// <summary>Retrieves the show command for a Shell link object</summary>
void GetShowCmd(out int piShowCmd);
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
void SetShowCmd(int iShowCmd);
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
int cchIconPath, out int piIcon);
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
/// <summary>Sets the relative path to the Shell link object</summary>
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags);
/// <summary>Sets the path and file name of a Shell link object</summary>
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;
}
}
}

View file

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Principal; using System.Security.Principal;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,11 +13,8 @@ using System.Windows.Media.Imaging;
using System.Xml.Linq; using System.Xml.Linq;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using Windows.Management.Deployment; using Windows.Management.Deployment;
using AppxPackaing;
using Shell;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using IStream = AppxPackaing.IStream;
using Rect = System.Windows.Rect; using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
@ -52,33 +50,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
private void InitializeAppInfo() private void InitializeAppInfo()
{ {
AppxPackageHelper _helper = new AppxPackageHelper();
var path = Path.Combine(Location, "AppxManifest.xml"); var path = Path.Combine(Location, "AppxManifest.xml");
var namespaces = XmlNamespaces(path); var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces); InitPackageVersion(namespaces);
var appxFactory = new AppxFactory();
IStream stream;
const uint noAttribute = 0x80; const uint noAttribute = 0x80;
const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive; const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive;
var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out stream); var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out IStream stream);
if (hResult == Hresult.Ok) if (hResult == Hresult.Ok)
{ {
var reader = appxFactory.CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
var apps = new List<Application>(); var apps = new List<Application>();
while (manifestApps.GetHasCurrent() != 0)
List<AppxPackageHelper.IAppxManifestApplication> _apps = _helper.getAppsFromManifest(stream);
foreach(var _app in _apps)
{ {
var manifestApp = manifestApps.GetCurrent(); var app = new Application(_app, this);
var appListEntry = manifestApp.GetStringValue("AppListEntry"); apps.Add(app);
if (appListEntry != "none")
{
var app = new Application(manifestApp, this);
apps.Add(app);
}
manifestApps.MoveNext();
} }
Apps = apps.Where(a => a.AppListEntry != "none").ToArray(); Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
} }
else else
@ -262,6 +254,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string UserModelId { get; set; } public string UserModelId { get; set; }
public string BackgroundColor { get; set; } public string BackgroundColor { get; set; }
public string EntryPoint { get; set; }
public string Name => DisplayName; public string Name => DisplayName;
public string Location => Package.Location; public string Location => Package.Location;
@ -358,15 +351,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
private async void Launch(IPublicAPI api) private async void Launch(IPublicAPI api)
{ {
var appManager = new ApplicationActivationManager(); var appManager = new ApplicationActivationHelper.ApplicationActivationManager();
uint unusedPid;
const string noArgs = ""; const string noArgs = "";
const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None;
await Task.Run(() => await Task.Run(() =>
{ {
try try
{ {
appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid); appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _);
} }
catch (Exception) catch (Exception)
{ {
@ -377,13 +369,24 @@ namespace Flow.Launcher.Plugin.Program.Programs
}); });
} }
public Application(IAppxManifestApplication manifestApp, UWP package) public Application(AppxPackageHelper.IAppxManifestApplication manifestApp, UWP package)
{ {
UserModelId = manifestApp.GetAppUserModelId(); // This is done because we cannot use the keyword 'out' along with a property
UniqueIdentifier = manifestApp.GetAppUserModelId();
DisplayName = manifestApp.GetStringValue("DisplayName"); manifestApp.GetAppUserModelId(out string tmpUserModelId);
Description = manifestApp.GetStringValue("Description"); manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier);
BackgroundColor = manifestApp.GetStringValue("BackgroundColor"); manifestApp.GetStringValue("DisplayName", out string tmpDisplayName);
manifestApp.GetStringValue("Description", out string tmpDescription);
manifestApp.GetStringValue("BackgroundColor", out string tmpBackgroundColor);
manifestApp.GetStringValue("EntryPoint", out string tmpEntryPoint);
UserModelId = tmpUserModelId;
UniqueIdentifier = tmpUniqueIdentifier;
DisplayName = tmpDisplayName;
Description = tmpDescription;
BackgroundColor = tmpBackgroundColor;
EntryPoint = tmpEntryPoint;
Package = package; Package = package;
DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName); DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName);
@ -451,7 +454,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return $"{prefix}//{packageName}{key}"; return $"{prefix}//{packageName}{key}";
} }
internal string LogoUriFromManifest(IAppxManifestApplication app) internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app)
{ {
var logoKeyFromVersion = new Dictionary<PackageVersion, string> var logoKeyFromVersion = new Dictionary<PackageVersion, string>
{ {
@ -462,7 +465,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (logoKeyFromVersion.ContainsKey(Package.Version)) if (logoKeyFromVersion.ContainsKey(Package.Version))
{ {
var key = logoKeyFromVersion[Package.Version]; var key = logoKeyFromVersion[Package.Version];
var logoUri = app.GetStringValue(key); app.GetStringValue(key, out string logoUri);
return logoUri; return logoUri;
} }
else else

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -8,11 +7,13 @@ using System.Security;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Win32; using Microsoft.Win32;
using Shell;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels; 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 namespace Flow.Launcher.Plugin.Program.Programs
{ {
@ -23,6 +24,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string UniqueIdentifier { get; set; } public string UniqueIdentifier { get; set; }
public string IcoPath { get; set; } public string IcoPath { get; set; }
public string FullPath { get; set; } public string FullPath { get; set; }
public string LnkResolvedPath { get; set; }
public string ParentDirectory { get; set; } public string ParentDirectory { get; set; }
public string ExecutableName { get; set; } public string ExecutableName { get; set; }
public string Description { get; set; } public string Description { get; set; }
@ -31,49 +33,64 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string Location => ParentDirectory; public string Location => ParentDirectory;
private const string ShortcutExtension = "lnk"; private const string ShortcutExtension = "lnk";
private const string ApplicationReferenceExtension = "appref-ms";
private const string ExeExtension = "exe"; private const string ExeExtension = "exe";
public Result Result(string query, IPublicAPI api) public Result Result(string query, IPublicAPI api)
{ {
string title; string title;
MatchResult matchResult;
var nameMatchResult = StringMatcher.FuzzySearch(query, Name);
var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description);
var pathMatchResult = new MatchResult(false, 0, new List<int>(), 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 // We suppose Name won't be null
if (Description == null || Name.StartsWith(Description)) if (Description == null || Name.StartsWith(Description))
{ {
title = Name; title = Name;
matchResult = StringMatcher.FuzzySearch(query, title);
} }
else if (Description.StartsWith(Name)) else if (Description.StartsWith(Name))
{ {
title = Description; title = Description;
matchResult = StringMatcher.FuzzySearch(query, Description);
} }
else else
{ {
title = $"{Name}: {Description}"; title = $"{Name}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, Name);
var desciptionMatch = StringMatcher.FuzzySearch(query, Description); if (matchResult == descriptionMatchResult)
if (desciptionMatch.Score > nameMatch.Score)
{ {
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<int>();
}
var result = new Result var result = new Result
{ {
Title = title, Title = title,
SubTitle = FullPath, SubTitle = LnkResolvedPath ?? FullPath,
IcoPath = IcoPath, IcoPath = IcoPath,
Score = matchResult.Score, Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData, TitleHighlightData = matchResult.MatchData,
@ -82,7 +99,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
var info = new ProcessStartInfo var info = new ProcessStartInfo
{ {
FileName = FullPath, FileName = LnkResolvedPath ?? FullPath,
WorkingDirectory = ParentDirectory, WorkingDirectory = ParentDirectory,
UseShellExecute = true UseShellExecute = true
}; };
@ -144,19 +161,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
Action = _ => Action = _ =>
{ {
var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs) var args = !string.IsNullOrWhiteSpace(Main._settings.CustomizedArgs)
? Main._settings.CustomizedArgs ? Main._settings.CustomizedArgs
.Replace("%s",$"\"{ParentDirectory}\"") .Replace("%s", $"\"{ParentDirectory}\"")
.Replace("%f",$"\"{FullPath}\"") .Replace("%f", $"\"{FullPath}\"")
: Main._settings.CustomizedExplorer==Settings.Explorer : Main._settings.CustomizedExplorer == Settings.Explorer
? $"/select,\"{FullPath}\"" ? $"/select,\"{FullPath}\""
: Settings.ExplorerArgs; : Settings.ExplorerArgs;
Main.StartProcess(Process.Start, Main.StartProcess(Process.Start,
new ProcessStartInfo( new ProcessStartInfo(
!string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer) !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer)
? Main._settings.CustomizedExplorer ? Main._settings.CustomizedExplorer
: Settings.Explorer, : Settings.Explorer,
args)); args));
return true; return true;
}, },
@ -167,10 +184,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
public override string ToString() public override string ToString()
{ {
return ExecutableName; return Name;
} }
private static Win32 Win32Program(string path) private static Win32 Win32Program(string path)
@ -193,7 +209,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{ {
ProgramLogger.LogException($"|Win32|Win32Program|{path}" + 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 }; return new Win32() { Valid = false, Enabled = false };
} }
@ -204,27 +220,21 @@ namespace Flow.Launcher.Plugin.Program.Programs
var program = Win32Program(path); var program = Win32Program(path);
try 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; const int MAX_PATH = 260;
StringBuilder buffer = new StringBuilder(MAX_PATH); 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)) if (!string.IsNullOrEmpty(target))
{ {
var extension = Extension(target); var extension = Extension(target);
if (extension == ExeExtension && File.Exists(target)) if (extension == ExeExtension && File.Exists(target))
{ {
buffer = new StringBuilder(MAX_PATH); program.LnkResolvedPath = program.FullPath;
link.GetDescription(buffer, MAX_PATH); program.FullPath = Path.GetFullPath(target).ToLower();
var description = buffer.ToString(); program.ExecutableName = Path.GetFileName(target);
var description = _helper.description;
if (!string.IsNullOrEmpty(description)) if (!string.IsNullOrEmpty(description))
{ {
program.Description = description; program.Description = description;
@ -239,13 +249,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
} }
return program; return program;
} }
catch (COMException e) catch (COMException e)
{ {
// C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + 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; program.Valid = false;
return program; return program;
@ -275,7 +287,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{ {
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + 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 }; return new Win32() { Valid = false, Enabled = false };
} }
@ -284,28 +296,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes) private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes)
{ {
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))
return new string[] { }; return Enumerable.Empty<string>();
try
{
var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true
})
.Where(x => suffixes.Contains(Extension(x)));
return paths; return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
}
catch (DirectoryNotFoundException e)
{ {
ProgramLogger.LogException($"Directory not found {directory}", e); IgnoreInaccessible = true,
return new string[] { }; RecurseSubdirectories = true
} }).Where(x => suffixes.Contains(Extension(x)));
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.LogException($"Permission denied {directory}", e);
return new string[] { };
}
} }
private static string Extension(string path) private static string Extension(string path)
@ -321,14 +318,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
private static ParallelQuery<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes) private static IEnumerable<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes)
{ {
var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled) var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
.SelectMany(s => ProgramPaths(s.Location, suffixes)) .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x)
.Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier))
.Distinct(); .Distinct();
var programs = paths.AsParallel().Select(x => Extension(x) switch var programs = paths.Select(x => Extension(x) switch
{ {
ExeExtension => ExeProgram(x), ExeExtension => ExeProgram(x),
ShortcutExtension => LnkProgram(x), ShortcutExtension => LnkProgram(x),
@ -339,7 +335,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return programs; return programs;
} }
private static ParallelQuery<Win32> StartMenuPrograms(string[] suffixes) private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes)
{ {
var disabledProgramsList = Main._settings.DisabledProgramSources; var disabledProgramsList = Main._settings.DisabledProgramSources;
@ -350,53 +346,49 @@ namespace Flow.Launcher.Plugin.Program.Programs
var toFilter = paths1.Concat(paths2); var toFilter = paths1.Concat(paths2);
var programs = toFilter var programs = ExceptDisabledSource(toFilter.Distinct())
.AsParallel() .Select(x => Extension(x) switch
.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1)) {
.Distinct() ShortcutExtension => LnkProgram(x),
.Select(x => Extension(x) switch _ => Win32Program(x)
{ }).Where(x => x.Valid);
ShortcutExtension => LnkProgram(x),
_ => Win32Program(x)
}).Where(x => x.Valid);
return programs; return programs;
} }
private static ParallelQuery<Win32> AppPathsPrograms(string[] suffixes) private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes)
{ {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
var programs = new List<Win32>();
using (var root = Registry.LocalMachine.OpenSubKey(appPaths)) IEnumerable<string> toFilter = Enumerable.Empty<string>();
using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths);
using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths);
if (rootMachine != null)
{ {
if (root != null) toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine));
{
programs.AddRange(GetProgramsFromRegistry(root));
}
}
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
{
if (root != null)
{
programs.AddRange(GetProgramsFromRegistry(root));
}
} }
var disabledProgramsList = Main._settings.DisabledProgramSources; if (rootUser != null)
var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName))); {
toFilter = toFilter.Concat(GetPathFromRegistry(rootUser));
}
var filtered = toFilter.Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)).Select(t1 => t1);
return filtered; 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<Win32> GetProgramsFromRegistry(RegistryKey root) private static IEnumerable<string> GetPathFromRegistry(RegistryKey root)
{ {
return root return root
.GetSubKeyNames() .GetSubKeyNames()
.Select(x => GetProgramPathFromRegistrySubKeys(root, x)) .Select(x => GetProgramPathFromRegistrySubKeys(root, x))
.Distinct() .Distinct();
.Select(x => GetProgramFromPath(x));
} }
private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey) private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey)
@ -422,7 +414,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{ {
ProgramLogger.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" + 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; return string.Empty;
} }
@ -431,27 +423,74 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static Win32 GetProgramFromPath(string path) private static Win32 GetProgramFromPath(string path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
return new Win32(); return null;
path = Environment.ExpandEnvironmentVariables(path); path = Environment.ExpandEnvironmentVariables(path);
if (!File.Exists(path)) if (!File.Exists(path))
return new Win32(); return null;
var entry = Win32Program(path); var entry = Win32Program(path);
entry.ExecutableName = Path.GetFileName(path);
return entry; 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) public static Win32[] All(Settings settings)
{ {
try try
{ {
var programs = new List<Win32>().AsParallel(); var programs = Enumerable.Empty<Win32>();
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
programs = programs.Concat(unregistered); programs = programs.Concat(unregistered);
if (settings.EnableRegistrySource) if (settings.EnableRegistrySource)
{ {
var appPaths = AppPathsPrograms(settings.ProgramSuffixes); var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
@ -464,7 +503,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
programs = programs.Concat(startMenu); 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. #if DEBUG //This is to make developer aware of any unhandled exception and add in handling.
catch (Exception e) catch (Exception e)
@ -478,9 +518,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e); ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e);
return new Win32[0]; return Array.Empty<Win32>();
} }
#endif #endif
} }
} }
} }

View file

@ -4,7 +4,7 @@
"Name": "Program", "Name": "Program",
"Description": "Search programs in Flow.Launcher", "Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "1.4.0", "Version": "1.4.2",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",

View file

@ -38,10 +38,11 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up. Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up.
**Integrations** **Integrations**
- If you use Python plugins: - Python plugins:
- Install [Python3](https://www.python.org/downloads/), download `.exe` installer. - Once a Python plugin has been detected at start up, you will be prompted to either select the location or allow Python 3.9.1 to automatic download and install.
- Add Python to `%PATH%` or set it in flow's settings. - Add Python to `%PATH%` or set it in flow's settings.
- Use `pip` to install `flowlauncher`, cmd in `pip install flowlauncher`. - Use `pip` to install `flowlauncher`, open cmd and type `pip install flowlauncher`.
- The Python plugin may require additional modules to be installed, please ensure you check by visiting the plugin's website via `pm install` + plugin name, go to context menu and select `Open website`.
- Start to launch your Python plugins. - Start to launch your Python plugins.
- Flow searches files and contents via Windows Index Search, to use Everything, download the plugin [here](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Everything/releases/latest). - Flow searches files and contents via Windows Index Search, to use Everything, download the plugin [here](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Everything/releases/latest).
@ -51,6 +52,7 @@ Windows may complain about security due to code not being signed, this will be c
- Open context menu: on the selected result, press <kbd>Ctrl</kbd>+<kbd>O</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>. - Open context menu: on the selected result, press <kbd>Ctrl</kbd>+<kbd>O</kbd>/<kbd>Shift</kbd>+<kbd>Enter</kbd>.
- Cancel/Return to previous screen: <kbd>Esc</kbd>. - Cancel/Return to previous screen: <kbd>Esc</kbd>.
- Install/Uninstall/Update plugins: in the search window, type `pm install`/`pm uninstall`/`pm update` + the plugin name. - Install/Uninstall/Update plugins: in the search window, type `pm install`/`pm uninstall`/`pm update` + the plugin name.
- Press `F5` while in the query window to reload all plugin data.
- Saved user settings are located: - Saved user settings are located:
- If using roaming: `%APPDATA%\FlowLauncher` - If using roaming: `%APPDATA%\FlowLauncher`
- If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData` - If using portable, by default: `%localappdata%\FlowLauncher\app-<VersionOfYourFlowLauncher>\UserData`