diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 2b2bef4cf..8d1408449 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -53,7 +53,7 @@
-
+
diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
index b1c8ff6ea..2b3bd61b0 100644
--- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
+++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
@@ -1,5 +1,4 @@
-
-/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
+/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins,
* like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the
* "id" and "jsonrpc" in the request, since it's not so useful in our request model.
*
@@ -13,10 +12,12 @@
*
*/
+using Flow.Launcher.Core.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
+using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
@@ -51,42 +52,15 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCRequestModel : JsonRPCModelBase
{
+ [JsonPropertyName("method")]
public string Method { get; set; }
+ [JsonPropertyName("parameters")]
public object[] Parameters { get; set; }
public override string ToString()
{
- string rpc = string.Empty;
- if (Parameters != null && Parameters.Length > 0)
- {
- string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]";
- rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}";
- }
- else
- {
- rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]";
- }
-
- return rpc;
-
- }
-
- private string GetParameterByType(object parameter)
- => parameter switch
- {
- null => "null",
- string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""",
- bool _ => $@"{parameter.ToString().ToLower()}",
- _ => parameter.ToString()
- };
-
-
- private string ReplaceEscapes(string str)
- {
- return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo
- .Replace(@"\", @"\\") //Escapes itself when passed to client
- .Replace(@"""", @"\\""""");
+ return JsonSerializer.Serialize(this);
}
}
@@ -95,11 +69,7 @@ namespace Flow.Launcher.Core.Plugin
///
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
- public override string ToString()
- {
- string rpc = base.ToString();
- return rpc + "}";
- }
+
}
///
@@ -107,13 +77,8 @@ namespace Flow.Launcher.Core.Plugin
///
public class JsonRPCClientRequestModel : JsonRPCRequestModel
{
+ [JsonPropertyName("dontHideAfterAction")]
public bool DontHideAfterAction { get; set; }
-
- public override string ToString()
- {
- string rpc = base.ToString();
- return rpc + "}";
- }
}
///
@@ -125,4 +90,4 @@ namespace Flow.Launcher.Core.Plugin
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
index 7a088bd09..ff9c11a94 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
@@ -1,4 +1,5 @@
-using System;
+using Flow.Launcher.Core.Resource;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -45,14 +46,20 @@ namespace Flow.Launcher.Core.Plugin
}
}
-
+ private static readonly JsonSerializerOptions _options = new()
+ {
+ Converters =
+ {
+ new JsonObjectConverter()
+ }
+ };
private async Task> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
- JsonRPCQueryResponseModel queryResponseModel = await
- JsonSerializer.DeserializeAsync(output);
+ var queryResponseModel = await
+ JsonSerializer.DeserializeAsync(output, _options);
return ParseResults(queryResponseModel);
}
@@ -61,17 +68,18 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(output)) return null;
- JsonRPCQueryResponseModel queryResponseModel =
- JsonSerializer.Deserialize(output);
+ var queryResponseModel =
+ JsonSerializer.Deserialize(output, _options);
return ParseResults(queryResponseModel);
}
+
private List ParseResults(JsonRPCQueryResponseModel queryResponseModel)
{
var results = new List();
if (queryResponseModel.Result == null) return null;
- if(!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
+ if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
{
context.API.ShowMsg(queryResponseModel.DebugMessage);
}
@@ -82,25 +90,31 @@ namespace Flow.Launcher.Core.Plugin
{
if (result.JsonRPCAction == null) return false;
- if (!string.IsNullOrEmpty(result.JsonRPCAction.Method))
+ if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
{
- if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
+ return !result.JsonRPCAction.DontHideAfterAction;
+ }
+
+ if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
+ {
+ ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
+ result.JsonRPCAction.Parameters);
+ }
+ else
+ {
+ var actionResponse = ExecuteCallback(result.JsonRPCAction);
+
+ if (string.IsNullOrEmpty(actionResponse))
{
- ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4),
- result.JsonRPCAction.Parameters);
+ return !result.JsonRPCAction.DontHideAfterAction;
}
- else
+
+ var jsonRpcRequestModel = JsonSerializer.Deserialize(actionResponse, _options);
+
+ if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
- string actionReponse = ExecuteCallback(result.JsonRPCAction);
- JsonRPCRequestModel jsonRpcRequestModel =
- JsonSerializer.Deserialize(actionReponse);
- if (jsonRpcRequestModel != null
- && !string.IsNullOrEmpty(jsonRpcRequestModel.Method)
- && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher."))
- {
- ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4),
- jsonRpcRequestModel.Parameters);
- }
+ ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
+ jsonRpcRequestModel.Parameters);
}
}
@@ -177,12 +191,14 @@ namespace Flow.Launcher.Core.Plugin
if (result.StartsWith("DEBUG:"))
{
- MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
+ MessageBox.Show(new Form
+ {
+ TopMost = true
+ }, result.Substring(6));
return string.Empty;
}
return result;
-
}
catch (Exception e)
{
@@ -248,10 +264,10 @@ namespace Flow.Launcher.Core.Plugin
}
}
- public Task InitAsync(PluginInitContext context)
+ public virtual Task InitAsync(PluginInitContext context)
{
this.context = context;
return Task.CompletedTask;
}
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index e2c009ee3..8d2c6df24 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin
private static IEnumerable _contextMenuPlugins;
public static List AllPlugins { get; private set; }
- public static readonly List GlobalPlugins = new List();
- public static readonly Dictionary NonGlobalPlugins = new Dictionary();
+ public static readonly HashSet GlobalPlugins = new();
+ public static readonly Dictionary NonGlobalPlugins = new ();
public static IPublicAPI API { private set; get; }
@@ -118,7 +118,9 @@ namespace Flow.Launcher.Core.Plugin
_contextMenuPlugins = GetPluginsForInterface();
foreach (var plugin in AllPlugins)
{
- foreach (var actionKeyword in plugin.Metadata.ActionKeywords)
+ // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
+ // has multiple global and action keywords because we will only add them here once.
+ foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
{
switch (actionKeyword)
{
@@ -141,7 +143,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
- public static List ValidPluginsForQuery(Query query)
+ public static ICollection ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
@@ -250,6 +252,8 @@ namespace Flow.Launcher.Core.Plugin
public static bool ActionKeywordRegistered(string actionKeyword)
{
+ // this method is only checking for action keywords (defined as not '*') registration
+ // hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
}
@@ -274,7 +278,7 @@ namespace Flow.Launcher.Core.Plugin
}
///
- /// used to add action keyword for multiple action keyword plugin
+ /// used to remove action keyword for multiple action keyword plugin
/// e.g. web search
///
public static void RemoveActionKeyword(string id, string oldActionkeyword)
@@ -283,9 +287,7 @@ namespace Flow.Launcher.Core.Plugin
if (oldActionkeyword == Query.GlobalPluginWildcardSign
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
plugin.Metadata.ActionKeywords
- .Where(x => x == Query.GlobalPluginWildcardSign)
- .ToList()
- .Count == 1)
+ .Count(x => x == Query.GlobalPluginWildcardSign) == 1)
{
GlobalPlugins.Remove(plugin);
}
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index 92aca8109..1b78c68ae 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
+using System.Diagnostics;
+using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin
{
public static class PluginsLoader
{
- public const string PATH = "PATH";
- public const string Python = "python";
public const string PythonExecutable = "pythonw.exe";
public static List Plugins(List metadatas, PluginsSettings settings)
@@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin
return;
}
- plugins.Add(new PluginPair
- {
- Plugin = plugin,
- Metadata = metadata
- });
+ plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
});
metadata.InitTime += milliseconds;
}
@@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin
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(settings.PythonDirectory))
- {
- var paths = Environment.GetEnvironmentVariable(PATH);
- if (paths != null)
- {
- var pythonInPath = paths
- .Split(';')
- .Where(p => p.ToLower().Contains(Python))
- .Any();
+ if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
+ return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
- if (pythonInPath)
+ var pythonPath = string.Empty;
+
+ 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))
{
- 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", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
+ 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
{
- var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
- if (File.Exists(path))
+ var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
+
+ // Python 3.8.9 is used for Windows 7 compatibility
+ DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
+
+ pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
+ if (FilesFolders.FileExists(pythonPath))
{
- Constant.PythonPath = path;
+ settings.PythonDirectory = installedPythonDirectory;
+ Constant.PythonPath = pythonPath;
}
else
{
- Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " +
- $"but can't find python executable in {path}", "PythonPlugins");
+ Log.Error("PluginsLoader",
+ $"Failed to set Python path after Droplex install, {pythonPath} does not exist",
+ "PythonPlugins");
}
}
- if (string.IsNullOrEmpty(settings.PythonDirectory))
+ if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(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).");
+ 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");
@@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin
return new List();
}
- return source
+ return SetPythonPathForPluginPairs(source, pythonPath);
+ }
+
+ private static IEnumerable SetPythonPathForPluginPairs(List source, string pythonPath)
+ => source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair
{
- Plugin = new PythonPlugin(Constant.PythonPath),
+ Plugin = new PythonPlugin(pythonPath),
Metadata = metadata
})
.ToList();
- }
- public static IEnumerable ExecutablePlugins(IEnumerable source)
+ public static IEnumerable ExecutablePlugins(IEnumerable source)
{
return source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
.Select(metadata => new PluginPair
{
- Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
- Metadata = metadata
+ Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
});
}
}
diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs
index 356a68a81..16d84136f 100644
--- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs
@@ -28,17 +28,19 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
+ //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
+ _startInfo.ArgumentList.Add("-B");
}
protected override Task ExecuteQueryAsync(Query query, CancellationToken token)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
- Method = "query",
- Parameters = new object[] { query.Search },
+ Method = "query", Parameters = new object[] {query.Search},
};
- //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
- _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
+
+ _startInfo.ArgumentList[2] = request.ToString();
+
// todo happlebao why context can't be used in constructor
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
@@ -47,16 +49,17 @@ namespace Flow.Launcher.Core.Plugin
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{
- _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
+ _startInfo.ArgumentList[2] = rpcRequest.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// TODO: Async Action
return Execute(_startInfo);
}
- protected override string ExecuteContextMenu(Result selectedResult) {
- JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
- Method = "context_menu",
- Parameters = new object[] { selectedResult.ContextData },
+ protected override string ExecuteContextMenu(Result selectedResult)
+ {
+ JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
+ {
+ Method = "context_menu", Parameters = new object[] {selectedResult.ContextData},
};
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
@@ -64,5 +67,13 @@ namespace Flow.Launcher.Core.Plugin
// TODO: Async Action
return Execute(_startInfo);
}
+
+ public override Task InitAsync(PluginInitContext context)
+ {
+ this.context = context;
+ _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
+ _startInfo.ArgumentList.Add("");
+ return Task.CompletedTask;
+ }
}
}
\ No newline at end of file
diff --git a/Flow.Launcher.Core/Resource/JsonObjectConverter.cs b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs
new file mode 100644
index 000000000..5a891ae30
--- /dev/null
+++ b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Flow.Launcher.Core.Resource
+{
+ public class JsonObjectConverter : JsonConverter