diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 8eb411707..6b8902b7b 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -53,6 +53,7 @@
+
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index fcf178445..2c75f9633 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Runtime.Loader;
using System.Threading.Tasks;
using System.Windows.Forms;
+using Droplex;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.Plugin
{
@@ -22,7 +23,7 @@ namespace Flow.Launcher.Core.Plugin
public static List Plugins(List metadatas, PluginsSettings settings)
{
var dotnetPlugins = DotNetPlugins(metadatas);
- var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
+ var pythonPlugins = PythonPlugins(metadatas, settings);
var executablePlugins = ExecutablePlugins(metadatas);
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
return plugins;
@@ -113,11 +114,14 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
- public static IEnumerable PythonPlugins(List source, string pythonDirectory)
+ public static IEnumerable PythonPlugins(List source, PluginsSettings settings)
{
- // try to set Constant.PythonPath, either from
+ if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
+ return new List();
+
+ // Try setting Constant.PythonPath first, either from
// PATH or from the given pythonDirectory
- if (string.IsNullOrEmpty(pythonDirectory))
+ if (string.IsNullOrEmpty(settings.PythonDirectory))
{
var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null)
@@ -129,47 +133,103 @@ namespace Flow.Launcher.Core.Plugin
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
{
- 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
{
- var path = Path.Combine(pythonDirectory, PythonExecutable);
+ var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
if (File.Exists(path))
{
Constant.PythonPath = path;
}
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,
- // load every python plugin pair.
- if (String.IsNullOrEmpty(Constant.PythonPath))
+ if (string.IsNullOrEmpty(settings.PythonDirectory))
{
+ 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();
}
- else
- {
- return source
- .Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
- .Select(metadata => new PluginPair
- {
- Plugin = new PythonPlugin(Constant.PythonPath),
- Metadata = metadata
- });
- }
+
+ return source
+ .Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
+ .Select(metadata => new PluginPair
+ {
+ Plugin = new PythonPlugin(Constant.PythonPath),
+ Metadata = metadata
+ })
+ .ToList();
}
public static IEnumerable ExecutablePlugins(IEnumerable source)
diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs
index de2e82359..3a3e770a5 100644
--- a/Flow.Launcher.Infrastructure/Http/Http.cs
+++ b/Flow.Launcher.Infrastructure/Http/Http.cs
@@ -75,7 +75,7 @@ namespace Flow.Launcher.Infrastructure.Http
{
try
{
- using var response = await client.GetAsync(url, token);
+ using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (response.StatusCode == HttpStatusCode.OK)
{
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
@@ -135,7 +135,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default)
{
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();
}
}
diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
index f0e4a79fc..37cfec252 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -12,7 +12,7 @@ namespace Flow.Launcher.Infrastructure.Storage
public class JsonStrorage where T : new()
{
private readonly JsonSerializerOptions _serializerSettings;
- private T _data;
+ protected T _data;
// need a new directory name
public const string DirectoryName = "Settings";
public const string FileSuffix = ".json";
diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
index 5418e1837..ca7c454c4 100644
--- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
@@ -15,5 +15,10 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
}
+
+ public PluginJsonStorage(T data) : this()
+ {
+ _data = data;
+ }
}
}
diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/IPublicAPI.cs
index dd73eb0e5..07e2b2700 100644
--- a/Flow.Launcher.Plugin/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/IPublicAPI.cs
@@ -3,6 +3,7 @@ using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.IO;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -88,17 +89,86 @@ namespace Flow.Launcher.Plugin
///
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
+ ///
+ /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
+ ///
+ /// Query string
+ /// The string that will be compared against the query
+ /// Match results
MatchResult FuzzySearch(string query, string stringToCompare);
+ ///
+ /// Http download the spefic url and return as string
+ ///
+ /// URL to call Http Get
+ /// Cancellation Token
+ /// Task to get string result
Task HttpGetStringAsync(string url, CancellationToken token = default);
+ ///
+ /// Http download the spefic url and return as stream
+ ///
+ /// URL to call Http Get
+ /// Cancellation Token
+ /// Task to get stream result
Task HttpGetStreamAsync(string url, CancellationToken token = default);
- Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath);
+ ///
+ /// Download the specific url to a cretain file path
+ ///
+ /// URL to download file
+ /// place to store file
+ /// Task showing the progress
+ Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
+ ///
+ /// Add ActionKeyword for specific plugin
+ ///
+ /// ID for plugin that needs to add action keyword
+ /// The actionkeyword that is supposed to be added
void AddActionKeyword(string pluginId, string newActionKeyword);
+ ///
+ /// Remove ActionKeyword for specific plugin
+ ///
+ /// ID for plugin that needs to remove action keyword
+ /// The actionkeyword that is supposed to be removed
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
+ ///
+ /// Log debug message
+ /// Message will only be logged in Debug mode
+ ///
+ void LogDebug(string className, string message, [CallerMemberName] string methodName = "");
+
+ ///
+ /// Log info message
+ ///
+ void LogInfo(string className, string message, [CallerMemberName] string methodName = "");
+
+ ///
+ /// Log warning message
+ ///
+ void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
+
+ ///
+ /// 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
+ ///
+ void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
+
+ ///
+ /// Load JsonStorage for current plugin. This is the method used to load settings from json in Flow
+ ///
+ /// Type for deserialization
+ ///
+ T LoadJsonStorage() where T : new();
+
+ ///
+ /// Save JsonStorage for current plugin. This is the method used to save settings to json in Flow
+ ///
+ /// Type for Serialization
+ ///
+ void SaveJsonStorage(T setting) where T : new();
}
}
diff --git a/Flow.Launcher.Infrastructure/Storage/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs
similarity index 81%
rename from Flow.Launcher.Infrastructure/Storage/ISavable.cs
rename to Flow.Launcher.Plugin/Interfaces/ISavable.cs
index 8294a3df8..7c1110e0e 100644
--- a/Flow.Launcher.Infrastructure/Storage/ISavable.cs
+++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs
@@ -1,4 +1,4 @@
-namespace Flow.Launcher.Infrastructure.Storage
+namespace Flow.Launcher.Plugin
{
///
/// Save plugin settings/cache,
diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index f8459830c..3b9a56652 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -66,6 +66,9 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/Flow.Launcher/Images/app.ico b/Flow.Launcher/Images/app.ico
new file mode 100644
index 000000000..36b1d22d0
Binary files /dev/null and b/Flow.Launcher/Images/app.ico differ
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index b6bf76b7f..a036949fc 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -121,6 +121,7 @@
New Action Keyword can't be empty
This new Action Keyword is already assigned to another plugin, please choose a different one
Success
+ Completed successfully
Use * if you don't want to specify an action keyword
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index 4cc0b4428..aa0240dd4 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -34,6 +34,7 @@
+
diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs
index 4129ce28b..6bb2fc2dc 100644
--- a/Flow.Launcher/Msg.xaml.cs
+++ b/Flow.Launcher/Msg.xaml.cs
@@ -66,7 +66,7 @@ namespace Flow.Launcher
}
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 {
imgIco.Source = ImageLoader.Load(iconPath);
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index 427fd9fc6..9b41c2c05 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -18,6 +18,9 @@ using System.Threading;
using System.IO;
using Flow.Launcher.Infrastructure.Http;
using JetBrains.Annotations;
+using System.Runtime.CompilerServices;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Storage;
namespace Flow.Launcher
{
@@ -61,18 +64,15 @@ namespace Flow.Launcher
// which will cause ungraceful exit
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);
}
- public void RestarApp()
- {
- RestartApp();
- }
+ public void RestarApp() => RestartApp();
- public void CheckForNewUpdate()
- {
- _settingsVM.UpdateApp();
- }
+ public void CheckForNewUpdate() => _settingsVM.UpdateApp();
public void SaveAppAllSettings()
{
@@ -82,15 +82,9 @@ namespace Flow.Launcher
ImageLoader.Save();
}
- public Task ReloadAllPluginData()
- {
- return PluginManager.ReloadData();
- }
+ public Task ReloadAllPluginData() => PluginManager.ReloadData();
- public void ShowMsg(string title, string subTitle = "", string iconPath = "")
- {
- ShowMsg(title, subTitle, iconPath, true);
- }
+ public void ShowMsg(string title, string subTitle = "", string iconPath = "") => ShowMsg(title, subTitle, iconPath, true);
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
{
@@ -109,54 +103,40 @@ namespace Flow.Launcher
});
}
- public void StartLoadingBar()
- {
- _mainVM.ProgressBarVisibility = Visibility.Visible;
- }
+ public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible;
- public void StopLoadingBar()
- {
- _mainVM.ProgressBarVisibility = Visibility.Collapsed;
- }
+ public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
- public string GetTranslation(string key)
- {
- return InternationalizationManager.Instance.GetTranslation(key);
- }
+ public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key);
- public List GetAllPlugins()
- {
- return PluginManager.AllPlugins.ToList();
- }
-
- public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
+ public List GetAllPlugins() => PluginManager.AllPlugins.ToList();
public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare);
- public Task HttpGetStringAsync(string url, CancellationToken token = default)
- {
- return Http.GetAsync(url);
- }
+ public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url);
- public Task HttpGetStreamAsync(string url, CancellationToken token = default)
- {
- return Http.GetStreamAsync(url);
- }
+ public Task HttpGetStreamAsync(string url, CancellationToken token = default) => Http.GetStreamAsync(url);
- public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath)
- {
- return Http.DownloadAsync(url, filePath);
- }
+ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) => Http.DownloadAsync(url, filePath, token);
- public void AddActionKeyword(string pluginId, string newActionKeyword)
- {
- PluginManager.AddActionKeyword(pluginId, newActionKeyword);
- }
+ public void AddActionKeyword(string pluginId, string 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() where T : new() => new PluginJsonStorage().Load();
+
+ public void SaveJsonStorage(T setting) where T : new() => new PluginJsonStorage(setting).Save();
+
+ public event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
- public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
- {
- PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
- }
#endregion
#region Private Methods
diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml
index 4c7eac114..cb0ef2def 100644
--- a/Flow.Launcher/SettingWindow.xaml
+++ b/Flow.Launcher/SettingWindow.xaml
@@ -11,7 +11,7 @@
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
x:Class="Flow.Launcher.SettingWindow"
mc:Ignorable="d"
- Icon="Images\app.png"
+ Icon="Images\app.ico"
Title="{DynamicResource flowlauncher_settings}"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index afbe6e197..c1382e51e 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -19,6 +19,7 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger;
+using System.Threading.Channels;
namespace Flow.Launcher.ViewModel
{
@@ -46,7 +47,7 @@ namespace Flow.Launcher.ViewModel
private readonly Internationalization _translator = InternationalizationManager.Instance;
- private BufferBlock _resultsUpdateQueue;
+ private ChannelWriter _resultsUpdateChannelWriter;
private Task _resultsViewUpdateTask;
#endregion
@@ -85,29 +86,32 @@ namespace Flow.Launcher.ViewModel
private void RegisterViewUpdate()
{
- _resultsUpdateQueue = new BufferBlock();
+ var resultUpdateChannel = Channel.CreateUnbounded();
+ _resultsUpdateChannelWriter = resultUpdateChannel.Writer;
_resultsViewUpdateTask =
Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
-
async Task updateAction()
{
var queue = new Dictionary();
- 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);
- while (_resultsUpdateQueue.TryReceive(out var item))
+ while (channelReader.TryRead(out var item))
{
if (!item.Token.IsCancellationRequested)
queue[item.ID] = item;
}
UpdateResultView(queue.Values);
+ queue.Clear();
}
- }
- ;
+ Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends");
+ };
void continueAction(Task t)
{
@@ -115,8 +119,8 @@ namespace Flow.Launcher.ViewModel
throw t.Exception;
#else
Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}");
- _resultsViewUpdateTask =
- Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
+ _resultsViewUpdateTask =
+ Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted);
#endif
}
}
@@ -131,7 +135,10 @@ namespace Flow.Launcher.ViewModel
if (e.Query.RawQuery == QueryText) // TODO: allow cancellation
{
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;
}
});
+
+ 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
@@ -313,6 +339,7 @@ namespace Flow.Launcher.ViewModel
public ICommand LoadContextMenuCommand { get; set; }
public ICommand LoadHistoryCommand { get; set; }
public ICommand OpenResultCommand { get; set; }
+ public ICommand ReloadPluginDataCommand { get; set; }
public string OpenResultCommandModifiers { get; private set; }
@@ -512,9 +539,12 @@ namespace Flow.Launcher.ViewModel
await Task.Yield();
var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken);
- if (!currentCancellationToken.IsCancellationRequested && results != null)
- _resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query,
- currentCancellationToken));
+ if (currentCancellationToken.IsCancellationRequested || results == null) return;
+
+ if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken)))
+ {
+ Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
+ };
}
}, currentCancellationToken)
.ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception),
diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs
index be48f53c1..87d526fd6 100644
--- a/Flow.Launcher/ViewModel/ResultsForUpdate.cs
+++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs
@@ -6,7 +6,7 @@ using System.Threading;
namespace Flow.Launcher.ViewModel
{
- public class ResultsForUpdate
+ public struct ResultsForUpdate
{
public List Results { get; }
@@ -16,13 +16,6 @@ namespace Flow.Launcher.ViewModel
public Query Query { get; }
public CancellationToken Token { get; }
- public ResultsForUpdate(List results, string resultID, CancellationToken token)
- {
- Results = results;
- ID = resultID;
- Token = token;
- }
-
public ResultsForUpdate(List results, PluginMetadata metadata, Query query, CancellationToken token)
{
Results = results;
diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs
index feab3a751..41f16f4f2 100644
--- a/Flow.Launcher/ViewModel/ResultsViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs
@@ -183,13 +183,12 @@ namespace Flow.Launcher.ViewModel
private List NewResults(List newRawResults, string resultId)
{
if (newRawResults.Count == 0)
- return Results.ToList();
+ return Results;
- var results = Results as IEnumerable;
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)
.OrderByDescending(r => r.Result.Score)
.ToList();
@@ -198,11 +197,9 @@ namespace Flow.Launcher.ViewModel
private List NewResults(IEnumerable resultsForUpdates)
{
if (!resultsForUpdates.Any())
- return Results.ToList();
+ return Results;
- var results = Results as IEnumerable;
-
- return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID))
+ return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID))
.Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings)))
.OrderByDescending(rv => rv.Result.Score)
.ToList();
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png
new file mode 100644
index 000000000..0dee870b6
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Images/copylink.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml
index 3beccb5e7..f456e7495 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml
@@ -12,4 +12,6 @@
New tab
Set browser from path:
Choose
+ Copy url
+ Copy the bookmark's url to clipboard
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
index 47493654f..b889bb0d0 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
@@ -1,6 +1,9 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Windows;
using System.Windows.Controls;
+using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
@@ -9,7 +12,7 @@ using Flow.Launcher.Plugin.SharedCommands;
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;
@@ -60,7 +63,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
}
return true;
- }
+ },
+ ContextData = new BookmarkAttributes { Url = c.Url }
}).Where(r => r.Score > 0);
return returnList.ToList();
}
@@ -84,7 +88,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
}
return true;
- }
+ },
+ ContextData = new BookmarkAttributes { Url = c.Url }
}).ToList();
}
}
@@ -115,5 +120,39 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
{
_storage.Save();
}
+
+ public List LoadContextMenus(Result selectedResult)
+ {
+ return new List() {
+ 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; }
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
index b0c3d2e29..62311f514 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
@@ -4,7 +4,7 @@
"Name": "Browser Bookmarks",
"Description": "Search your browser bookmarks",
"Author": "qianlifeng, Ioannis G.",
- "Version": "1.3.2",
+ "Version": "1.4.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
index fe720d6dd..93038ab85 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj
@@ -18,6 +18,7 @@
+
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
index 66bfd2ab5..3707beee2 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
@@ -37,20 +37,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
Settings = viewModel.Settings;
contextMenu = new ContextMenu(Context);
pluginManager = new PluginsManager(Context, Settings);
- var updateManifestTask = pluginManager.UpdateManifest();
- _ = updateManifestTask.ContinueWith(t =>
- {
- if (t.IsCompletedSuccessfully)
- {
- 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);
- }
- });
+ _ = pluginManager.UpdateManifest().ContinueWith(_ =>
+ {
+ lastUpdateTime = DateTime.Now;
+ }, TaskContinuationOptions.OnlyOnRanToCompletion);
return Task.CompletedTask;
}
@@ -69,15 +59,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours
{
- await pluginManager.UpdateManifest();
- lastUpdateTime = DateTime.Now;
+ _ = pluginManager.UpdateManifest().ContinueWith(t =>
+ {
+ lastUpdateTime = DateTime.Now;
+ }, TaskContinuationOptions.OnlyOnRanToCompletion);
}
return search switch
{
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.HotkeyUpdate) => pluginManager.RequestUpdate(s),
+ var s when s.StartsWith(Settings.HotkeyUpdate) => await pluginManager.RequestUpdate(s, token),
_ => pluginManager.GetDefaultHotKeys().Where(hotkey =>
{
hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score;
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
index 881872fc1..c2e392b51 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
@@ -1,3 +1,4 @@
+using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
@@ -47,9 +48,23 @@ namespace Flow.Launcher.Plugin.PluginsManager
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 GetDefaultHotKeys()
@@ -150,8 +165,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.RestartApp();
}
- internal List RequestUpdate(string search)
+ internal async ValueTask> RequestUpdate(string search, CancellationToken token)
{
+ if (!pluginsManifest.UserPlugins.Any())
+ {
+ await UpdateManifest();
+ }
+
+ token.ThrowIfCancellationRequested();
+
var autocompletedResults = AutoCompleteReturnAllResults(search,
Settings.HotkeyUpdate,
"Update",
@@ -275,20 +297,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
.ToList();
}
- private Task _downloadManifestTask = Task.CompletedTask;
-
internal async ValueTask> RequestInstallOrUpdate(string searchName, CancellationToken token)
{
- if (!pluginsManifest.UserPlugins.Any() &&
- _downloadManifestTask.Status != TaskStatus.Running)
+ if (!pluginsManifest.UserPlugins.Any())
{
- _downloadManifestTask = pluginsManifest.DownloadManifest();
+ await UpdateManifest();
}
- await _downloadManifestTask;
-
- if (token.IsCancellationRequested)
- return null;
+ token.ThrowIfCancellationRequested();
var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim();
@@ -400,6 +416,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
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
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
}
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
index ad4601586..f95c0d60d 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
@@ -6,7 +6,7 @@
"Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Author": "Jeremy Wu",
- "Version": "1.6.3",
+ "Version": "1.7.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll b/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll
deleted file mode 100644
index 183cfc085..000000000
Binary files a/Plugins/Flow.Launcher.Plugin.Program/AppxPackagingTlb.dll and /dev/null differ
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
index 986ce218c..1bd39ba73 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
@@ -40,17 +40,6 @@
PreserveNewest
-
-
-
- .\AppxPackagingTlb.dll
- True
-
-
- .\ShObjIdlTlb.dll
- True
-
-
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index 22f4aea59..bd09cd9c6 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -228,7 +228,6 @@ namespace Flow.Launcher.Plugin.Program
public static void StartProcess(Func runProcess, ProcessStartInfo info)
{
- bool hide;
try
{
runProcess(info);
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs
new file mode 100644
index 000000000..4b8423ed6
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs
@@ -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);
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs
new file mode 100644
index 000000000..e48f30757
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs
@@ -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 getAppsFromManifest(IStream stream)
+ {
+ List apps = new List();
+ 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);
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
new file mode 100644
index 000000000..4ded3412a
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
@@ -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
+ {
+ /// Retrieves the path and file name of a Shell link object
+ void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
+ /// Retrieves the list of item identifiers for a Shell link object
+ void GetIDList(out IntPtr ppidl);
+ /// Sets the pointer to an item identifier list (PIDL) for a Shell link object.
+ void SetIDList(IntPtr pidl);
+ /// Retrieves the description string for a Shell link object
+ void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
+ /// Sets the description for a Shell link object. The description can be any application-defined string
+ void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
+ /// Retrieves the name of the working directory for a Shell link object
+ void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
+ /// Sets the name of the working directory for a Shell link object
+ void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
+ /// Retrieves the command-line arguments associated with a Shell link object
+ void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
+ /// Sets the command-line arguments for a Shell link object
+ void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
+ /// Retrieves the hot key for a Shell link object
+ void GetHotkey(out short pwHotkey);
+ /// Sets a hot key for a Shell link object
+ void SetHotkey(short wHotkey);
+ /// Retrieves the show command for a Shell link object
+ void GetShowCmd(out int piShowCmd);
+ /// Sets the show command for a Shell link object. The show command sets the initial show state of the window.
+ void SetShowCmd(int iShowCmd);
+ /// Retrieves the location (path and index) of the icon for a Shell link object
+ void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
+ int cchIconPath, out int piIcon);
+ /// Sets the location (path and index) of the icon for a Shell link object
+ void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
+ /// Sets the relative path to the Shell link object
+ void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
+ /// Attempts to find the target of a Shell link, even if it has been moved or renamed
+ void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags);
+ /// Sets the path and file name of a Shell link object
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
index 20d8f185b..640c95ba0 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
@@ -12,11 +13,8 @@ using System.Windows.Media.Imaging;
using System.Xml.Linq;
using Windows.ApplicationModel;
using Windows.Management.Deployment;
-using AppxPackaing;
-using Shell;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
-using IStream = AppxPackaing.IStream;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
@@ -52,33 +50,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
private void InitializeAppInfo()
{
+ AppxPackageHelper _helper = new AppxPackageHelper();
var path = Path.Combine(Location, "AppxManifest.xml");
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
- var appxFactory = new AppxFactory();
- IStream stream;
const uint noAttribute = 0x80;
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)
{
- var reader = appxFactory.CreateManifestReader(stream);
- var manifestApps = reader.GetApplications();
var apps = new List();
- while (manifestApps.GetHasCurrent() != 0)
+
+ List _apps = _helper.getAppsFromManifest(stream);
+ foreach(var _app in _apps)
{
- var manifestApp = manifestApps.GetCurrent();
- var appListEntry = manifestApp.GetStringValue("AppListEntry");
- if (appListEntry != "none")
- {
- var app = new Application(manifestApp, this);
- apps.Add(app);
- }
- manifestApps.MoveNext();
+ var app = new Application(_app, this);
+ apps.Add(app);
}
+
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
}
else
@@ -262,6 +254,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string UserModelId { get; set; }
public string BackgroundColor { get; set; }
+ public string EntryPoint { get; set; }
public string Name => DisplayName;
public string Location => Package.Location;
@@ -358,15 +351,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
private async void Launch(IPublicAPI api)
{
- var appManager = new ApplicationActivationManager();
- uint unusedPid;
+ var appManager = new ApplicationActivationHelper.ApplicationActivationManager();
const string noArgs = "";
- const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE;
+ const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None;
await Task.Run(() =>
{
try
{
- appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid);
+ appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _);
}
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();
- UniqueIdentifier = manifestApp.GetAppUserModelId();
- DisplayName = manifestApp.GetStringValue("DisplayName");
- Description = manifestApp.GetStringValue("Description");
- BackgroundColor = manifestApp.GetStringValue("BackgroundColor");
+ // This is done because we cannot use the keyword 'out' along with a property
+
+ manifestApp.GetAppUserModelId(out string tmpUserModelId);
+ manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier);
+ 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;
DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName);
@@ -451,7 +454,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return $"{prefix}//{packageName}{key}";
}
- internal string LogoUriFromManifest(IAppxManifestApplication app)
+ internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app)
{
var logoKeyFromVersion = new Dictionary
{
@@ -462,7 +465,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (logoKeyFromVersion.ContainsKey(Package.Version))
{
var key = logoKeyFromVersion[Package.Version];
- var logoUri = app.GetStringValue(key);
+ app.GetStringValue(key, out string logoUri);
return logoUri;
}
else
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index fd994aeb3..372d36524 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -8,11 +7,13 @@ using System.Security;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Win32;
-using Shell;
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
{
@@ -23,6 +24,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
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; }
@@ -31,49 +33,64 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string Location => ParentDirectory;
private const string ShortcutExtension = "lnk";
- private const string ApplicationReferenceExtension = "appref-ms";
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 = new MatchResult(false, 0, new List(), 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
if (Description == null || Name.StartsWith(Description))
{
title = Name;
- matchResult = StringMatcher.FuzzySearch(query, title);
}
else if (Description.StartsWith(Name))
{
title = Description;
- matchResult = StringMatcher.FuzzySearch(query, Description);
}
else
{
title = $"{Name}: {Description}";
- var nameMatch = StringMatcher.FuzzySearch(query, Name);
- var desciptionMatch = StringMatcher.FuzzySearch(query, Description);
- if (desciptionMatch.Score > nameMatch.Score)
+
+ if (matchResult == descriptionMatchResult)
{
- 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();
+ }
var result = new Result
{
Title = title,
- SubTitle = FullPath,
+ SubTitle = LnkResolvedPath ?? FullPath,
IcoPath = IcoPath,
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
@@ -82,7 +99,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
+ FileName = LnkResolvedPath ?? FullPath,
WorkingDirectory = ParentDirectory,
UseShellExecute = true
};
@@ -144,19 +161,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
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._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));
+ new ProcessStartInfo(
+ !string.IsNullOrWhiteSpace(Main._settings.CustomizedExplorer)
+ ? Main._settings.CustomizedExplorer
+ : Settings.Explorer,
+ args));
return true;
},
@@ -167,10 +184,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
-
public override string ToString()
{
- return ExecutableName;
+ return Name;
}
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)
{
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 };
}
@@ -204,27 +220,21 @@ namespace Flow.Launcher.Plugin.Program.Programs
var program = Win32Program(path);
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;
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))
{
var extension = Extension(target);
if (extension == ExeExtension && File.Exists(target))
{
- buffer = new StringBuilder(MAX_PATH);
- link.GetDescription(buffer, MAX_PATH);
- var description = buffer.ToString();
+ 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;
@@ -239,13 +249,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
}
+
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);
+ "|Error caused likely due to trying to get the description of the program",
+ e);
program.Valid = false;
return program;
@@ -275,7 +287,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
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);
+ $"|Permission denied when trying to load the program from {path}", e);
return new Win32() { Valid = false, Enabled = false };
}
@@ -284,28 +296,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable ProgramPaths(string directory, string[] suffixes)
{
if (!Directory.Exists(directory))
- return new string[] { };
- try
- {
- var paths = Directory.EnumerateFiles(directory, "*", new EnumerationOptions
- {
- IgnoreInaccessible = true,
- RecurseSubdirectories = true
- })
- .Where(x => suffixes.Contains(Extension(x)));
+ return Enumerable.Empty();
- return paths;
- }
- catch (DirectoryNotFoundException e)
+ return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{
- ProgramLogger.LogException($"Directory not found {directory}", e);
- return new string[] { };
- }
- catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
- {
- ProgramLogger.LogException($"Permission denied {directory}", e);
- return new string[] { };
- }
+ IgnoreInaccessible = true,
+ RecurseSubdirectories = true
+ }).Where(x => suffixes.Contains(Extension(x)));
}
private static string Extension(string path)
@@ -321,14 +318,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static ParallelQuery UnregisteredPrograms(List sources, string[] suffixes)
+ private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes)
{
- var paths = sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
- .SelectMany(s => ProgramPaths(s.Location, suffixes))
- .Where(t1 => !Main._settings.DisabledProgramSources.Any(x => t1 == x.UniqueIdentifier))
+ var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
+ .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x)
.Distinct();
- var programs = paths.AsParallel().Select(x => Extension(x) switch
+ var programs = paths.Select(x => Extension(x) switch
{
ExeExtension => ExeProgram(x),
ShortcutExtension => LnkProgram(x),
@@ -339,7 +335,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return programs;
}
- private static ParallelQuery StartMenuPrograms(string[] suffixes)
+ private static IEnumerable StartMenuPrograms(string[] suffixes)
{
var disabledProgramsList = Main._settings.DisabledProgramSources;
@@ -350,53 +346,49 @@ namespace Flow.Launcher.Plugin.Program.Programs
var toFilter = paths1.Concat(paths2);
- var programs = toFilter
- .AsParallel()
- .Where(t1 => !disabledProgramsList.Any(x => x.UniqueIdentifier == t1))
- .Distinct()
- .Select(x => Extension(x) switch
- {
- ShortcutExtension => LnkProgram(x),
- _ => Win32Program(x)
- }).Where(x => x.Valid);
+ var programs = ExceptDisabledSource(toFilter.Distinct())
+ .Select(x => Extension(x) switch
+ {
+ ShortcutExtension => LnkProgram(x),
+ _ => Win32Program(x)
+ }).Where(x => x.Valid);
return programs;
}
- private static ParallelQuery AppPathsPrograms(string[] suffixes)
+ private static IEnumerable AppPathsPrograms(string[] suffixes)
{
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
- var programs = new List();
- using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
+
+ IEnumerable toFilter = Enumerable.Empty();
+
+ using var rootMachine = Registry.LocalMachine.OpenSubKey(appPaths);
+ using var rootUser = Registry.CurrentUser.OpenSubKey(appPaths);
+
+ if (rootMachine != null)
{
- if (root != null)
- {
- programs.AddRange(GetProgramsFromRegistry(root));
- }
- }
- using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
- {
- if (root != null)
- {
- programs.AddRange(GetProgramsFromRegistry(root));
- }
+ toFilter = toFilter.Concat(GetPathFromRegistry(rootMachine));
}
- var disabledProgramsList = Main._settings.DisabledProgramSources;
- var toFilter = programs.AsParallel().Where(p => suffixes.Contains(Extension(p.ExecutableName)));
+ if (rootUser != null)
+ {
+ 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 GetProgramsFromRegistry(RegistryKey root)
+ private static IEnumerable GetPathFromRegistry(RegistryKey root)
{
return root
- .GetSubKeyNames()
- .Select(x => GetProgramPathFromRegistrySubKeys(root, x))
- .Distinct()
- .Select(x => GetProgramFromPath(x));
+ .GetSubKeyNames()
+ .Select(x => GetProgramPathFromRegistrySubKeys(root, x))
+ .Distinct();
}
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)
{
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;
}
@@ -431,27 +423,74 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static Win32 GetProgramFromPath(string path)
{
if (string.IsNullOrEmpty(path))
- return new Win32();
+ return null;
path = Environment.ExpandEnvironmentVariables(path);
if (!File.Exists(path))
- return new Win32();
+ return null;
var entry = Win32Program(path);
- entry.ExecutableName = Path.GetFileName(path);
return entry;
}
+ public static IEnumerable ExceptDisabledSource(IEnumerable sources)
+ {
+ return ExceptDisabledSource(sources, x => x);
+ }
+
+ public static IEnumerable ExceptDisabledSource(IEnumerable sources,
+ Func keySelector)
+ {
+ return Main._settings.DisabledProgramSources.Count == 0
+ ? sources
+ : ExceptDisabledSourceEnumerable(sources, keySelector);
+
+ static IEnumerable ExceptDisabledSourceEnumerable(IEnumerable elements,
+ Func 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 DistinctBy(IEnumerable source, Func selector)
+ {
+ var set = new HashSet();
+ foreach (var item in source)
+ {
+ if (set.Add(selector(item)))
+ yield return item;
+ }
+ }
+
+ private static Win32[] ProgramsHasher(IEnumerable 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 = new List().AsParallel();
+ var programs = Enumerable.Empty();
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
programs = programs.Concat(unregistered);
+
if (settings.EnableRegistrySource)
{
var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
@@ -464,7 +503,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
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.
catch (Exception e)
@@ -478,9 +518,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e);
- return new Win32[0];
+ return Array.Empty();
}
#endif
}
}
-}
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll b/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll
deleted file mode 100644
index 83815e40a..000000000
Binary files a/Plugins/Flow.Launcher.Plugin.Program/ShObjIdlTlb.dll and /dev/null differ
diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json
index f713a33ec..d110124ff 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json
@@ -4,7 +4,7 @@
"Name": "Program",
"Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng",
- "Version": "1.4.0",
+ "Version": "1.4.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
diff --git a/README.md b/README.md
index 9af47a5ee..8512142b0 100644
--- a/README.md
+++ b/README.md
@@ -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.
**Integrations**
- - If you use Python plugins:
- - Install [Python3](https://www.python.org/downloads/), download `.exe` installer.
+ - Python plugins:
+ - 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.
- - 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.
- 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 Ctrl+O/Shift+Enter.
- Cancel/Return to previous screen: Esc.
- 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:
- If using roaming: `%APPDATA%\FlowLauncher`
- If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData`