diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
index e3f0e2a2f..1e30895cc 100644
--- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
@@ -12,7 +12,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
public static class PluginsManifest
{
- private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json";
+ private const string manifestFileUrl = "https://jsdelivr.bobocdn.tk/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json";
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 35a998896..133ed02e3 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -64,9 +64,5 @@
-
-
-
-
\ No newline at end of file
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
index 85b474157..18f787018 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
@@ -41,9 +41,11 @@ namespace Flow.Launcher.Core.Plugin
private int RequestId { get; set; }
- private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
+ private string SettingConfigurationPath =>
+ Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
- private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
+ private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
+ Context.CurrentPluginMetadata.Name, "Settings.json");
public abstract List LoadContextMenus(Result selectedResult);
@@ -57,10 +59,7 @@ namespace Flow.Launcher.Core.Plugin
// see: https://github.com/dotnet/runtime/issues/39152
IgnoreNullValues = true,
#pragma warning restore SYSLIB0020 // Type or member is obsolete
- Converters =
- {
- new JsonObjectConverter()
- }
+ Converters = { new JsonObjectConverter() }
};
protected static readonly JsonSerializerOptions RequestSerializeOption = new()
@@ -83,9 +82,9 @@ namespace Flow.Launcher.Core.Plugin
foreach (var result in queryResponseModel.Result)
{
- result.AsyncAction = async c =>
+ result.AsyncAction = async _ =>
{
- Settings.UpdateSettings(result.SettingsChange);
+ Settings?.UpdateSettings(result.SettingsChange);
return await ExecuteResultAsync(result);
};
@@ -95,7 +94,7 @@ namespace Flow.Launcher.Core.Plugin
results.AddRange(queryResponseModel.Result);
- Settings.UpdateSettings(queryResponseModel.SettingsChanges);
+ Settings?.UpdateSettings(queryResponseModel.SettingsChanges);
return results;
}
@@ -130,18 +129,18 @@ namespace Flow.Launcher.Core.Plugin
if (!File.Exists(SettingConfigurationPath))
return;
- var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
- var configuration = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath));
+ var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .Build();
+ var configuration =
+ deserializer.Deserialize(
+ await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new PortableSettings
{
- Configuration = configuration,
- SettingPath = SettingPath,
- API = Context.API
+ Configuration = configuration, SettingPath = SettingPath, API = Context.API
};
await Settings.InitializeAsync();
-
}
public virtual async Task InitAsync(PluginInitContext context)
@@ -154,10 +153,10 @@ namespace Flow.Launcher.Core.Plugin
{
Settings?.Save();
}
+
public Control CreateSettingPanel()
{
return Settings.CreateSettingPanel();
}
}
-
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
index e1e79359d..df9be0d79 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
@@ -1,12 +1,8 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
-using System.Text.Json;
using System.Threading;
-using System.Threading.Channels;
using System.Threading.Tasks;
-using System.Windows.Controls;
using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
using Flow.Launcher.Plugin;
using StreamJsonRpc;
@@ -14,13 +10,13 @@ using StreamJsonRpc;
namespace Flow.Launcher.Core.Plugin
{
- internal abstract class JsonRpcPluginV2 : JsonRPCPluginBase
+ internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IDisposable
{
public abstract string SupportedLanguage { get; set; }
public const string JsonRpc = "JsonRPC";
- protected abstract JsonRpc Rpc { get; set; }
+ protected abstract JsonRpc RPC { get; set; }
protected StreamReader ErrorStream { get; set; }
@@ -29,7 +25,8 @@ namespace Flow.Launcher.Core.Plugin
{
try
{
- var res = await Rpc.InvokeAsync(result.JsonRPCAction.Method, argument: result.JsonRPCAction.Parameters);
+ var res = await RPC.InvokeAsync(result.JsonRPCAction.Method,
+ argument: result.JsonRPCAction.Parameters);
return res.Hide;
}
@@ -43,7 +40,9 @@ namespace Flow.Launcher.Core.Plugin
{
try
{
- var res = await Rpc.InvokeAsync("query", query);
+ var res = await RPC.InvokeWithCancellationAsync("query",
+ new[] { query },
+ token);
var results = ParseResults(res);
@@ -51,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch
{
- return new List();
+ return new List();
}
}
@@ -72,5 +71,11 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
+
+ public void Dispose()
+ {
+ RPC?.Dispose();
+ ErrorStream?.Dispose();
+ }
}
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs
index 6a130f70f..632bb9501 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs
@@ -1,4 +1,4 @@
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{
- public record JsonRPCExecuteResponse(bool Hide = true);
+ public abstract record JsonRPCExecuteResponse(bool Hide = true);
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
new file mode 100644
index 000000000..a65d5db22
--- /dev/null
+++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
@@ -0,0 +1,326 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
+
+namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
+{
+ public class JsonRPCPublicAPI
+ {
+ private IPublicAPI _api;
+
+ public JsonRPCPublicAPI(IPublicAPI api)
+ {
+ _api = api;
+ }
+
+ ///
+ /// Change Flow.Launcher query
+ ///
+ /// query text
+ ///
+ /// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
+ /// Set this to to force Flow Launcher requerying
+ ///
+ public void ChangeQuery(string query, bool requery = false)
+ {
+ _api.ChangeQuery(query, requery);
+ }
+
+ ///
+ /// Restart Flow Launcher
+ ///
+ public void RestartApp()
+ {
+ _api.RestartApp();
+ }
+
+ ///
+ /// Run a shell command
+ ///
+ /// The command or program to run
+ /// the shell type to run, e.g. powershell.exe
+ /// Thrown when unable to find the file specified in the command
+ /// Thrown when error occurs during the execution of the command
+ public void ShellRun(string cmd, string filename = "cmd.exe")
+ {
+ _api.ShellRun(cmd, filename);
+ }
+
+ ///
+ /// Copies the passed in text and shows a message indicating whether the operation was completed successfully.
+ /// When directCopy is set to true and passed in text is the path to a file or directory,
+ /// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard.
+ ///
+ /// Text to save on clipboard
+ /// When true it will directly copy the file/folder from the path specified in text
+ /// Whether to show the default notification from this method after copy is done.
+ /// It will show file/folder/text is copied successfully.
+ /// Turn this off to show your own notification after copy is done.>
+ public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true)
+ {
+ _api.CopyToClipboard(text, directCopy, showDefaultNotification);
+ }
+
+ ///
+ /// Save everything, all of Flow Launcher and plugins' data and settings
+ ///
+ public void SaveAppAllSettings()
+ {
+ _api.SaveAppAllSettings();
+ }
+
+ ///
+ /// Save all Flow's plugins settings
+ ///
+ public void SavePluginSettings()
+ {
+ _api.SavePluginSettings();
+ }
+
+ ///
+ /// Reloads any Plugins that have the
+ /// IReloadable implemented. It refeshes
+ /// Plugin's in memory data with new content
+ /// added by user.
+ ///
+ public Task ReloadAllPluginDataAsync()
+ {
+ return _api.ReloadAllPluginData();
+ }
+
+ ///
+ /// Check for new Flow Launcher update
+ ///
+ public void CheckForNewUpdate()
+ {
+ _api.CheckForNewUpdate();
+ }
+
+ ///
+ /// Show the error message using Flow's standard error icon.
+ ///
+ /// Message title
+ /// Optional message subtitle
+ public void ShowMsgError(string title, string subTitle = "")
+ {
+ _api.ShowMsgError(title, subTitle);
+ }
+
+ ///
+ /// Show the MainWindow when hiding
+ ///
+ public void ShowMainWindow()
+ {
+ _api.ShowMainWindow();
+ }
+
+ ///
+ /// Hide MainWindow
+ ///
+ public void HideMainWindow()
+ {
+ _api.HideMainWindow();
+ }
+
+ ///
+ /// Representing whether the main window is visible
+ ///
+ ///
+ public bool IsMainWindowVisible()
+ {
+ return _api.IsMainWindowVisible();
+ }
+
+ ///
+ /// Show message box
+ ///
+ /// Message title
+ /// Message subtitle
+ /// Message icon path (relative path to your plugin folder)
+ public void ShowMsg(string title, string subTitle = "", string iconPath = "")
+ {
+ _api.ShowMsg(title, subTitle, iconPath);
+ }
+
+ ///
+ /// Show message box
+ ///
+ /// Message title
+ /// Message subtitle
+ /// Message icon path (relative path to your plugin folder)
+ /// when true will use main windows as the owner
+ public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
+ {
+ _api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner);
+ }
+
+ ///
+ /// Open setting dialog
+ ///
+ public void OpenSettingDialog()
+ {
+ _api.OpenSettingDialog();
+ }
+
+ ///
+ /// Get translation of current language
+ /// You need to implement IPluginI18n if you want to support multiple languages for your plugin
+ ///
+ ///
+ ///
+ public string GetTranslation(string key)
+ {
+ return _api.GetTranslation(key);
+ }
+
+ ///
+ /// Get all loaded plugins
+ ///
+ ///
+ public List GetAllPlugins()
+ {
+ return _api.GetAllPlugins();
+ }
+
+
+ ///
+ /// 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
+ public MatchResult FuzzySearch(string query, string stringToCompare)
+ {
+ return _api.FuzzySearch(query, stringToCompare);
+ }
+
+ ///
+ /// Http download the spefic url and return as string
+ ///
+ /// URL to call Http Get
+ /// Cancellation Token
+ /// Task to get string result
+ public Task HttpGetStringAsync(string url, CancellationToken token = default)
+ {
+ return _api.HttpGetStringAsync(url, token);
+ }
+
+ ///
+ /// Http download the spefic url and return as stream
+ ///
+ /// URL to call Http Get
+ /// Cancellation Token
+ /// Task to get stream result
+ public Task HttpGetStreamAsync(string url, CancellationToken token = default)
+ {
+ return _api.HttpGetStreamAsync(url, token);
+ }
+
+ ///
+ /// Download the specific url to a cretain file path
+ ///
+ /// URL to download file
+ /// path to save downloaded file
+ /// place to store file
+ /// Task showing the progress
+ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
+ CancellationToken token = default)
+ {
+ return _api.HttpDownloadAsync(url, filePath, token);
+ }
+
+ ///
+ /// Add ActionKeyword for specific plugin
+ ///
+ /// ID for plugin that needs to add action keyword
+ /// The actionkeyword that is supposed to be added
+ public void AddActionKeyword(string pluginId, string newActionKeyword)
+ {
+ _api.AddActionKeyword(pluginId, newActionKeyword);
+ }
+
+ ///
+ /// Remove ActionKeyword for specific plugin
+ ///
+ /// ID for plugin that needs to remove action keyword
+ /// The actionkeyword that is supposed to be removed
+ public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
+ {
+ _api.RemoveActionKeyword(pluginId, oldActionKeyword);
+ }
+
+ ///
+ /// Check whether specific ActionKeyword is assigned to any of the plugin
+ ///
+ /// The actionkeyword for checking
+ /// True if the actionkeyword is already assigned, False otherwise
+ public bool ActionKeywordAssigned(string actionKeyword)
+ {
+ return _api.ActionKeywordAssigned(actionKeyword);
+ }
+
+ ///
+ /// Log debug message
+ /// Message will only be logged in Debug mode
+ ///
+ public void LogDebug(string className, string message, [CallerMemberName] string methodName = "")
+ {
+ _api.LogDebug(className, message, methodName);
+ }
+
+ ///
+ /// Log info message
+ ///
+ public void LogInfo(string className, string message, [CallerMemberName] string methodName = "")
+ {
+ _api.LogInfo(className, message, methodName);
+ }
+
+ ///
+ /// Log warning message
+ ///
+ public void LogWarn(string className, string message, [CallerMemberName] string methodName = "")
+ {
+ _api.LogWarn(className, message, methodName);
+ }
+
+
+ ///
+ /// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer
+ ///
+ /// Directory Path to open
+ /// Extra FileName Info
+ public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
+ {
+ _api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
+ }
+
+
+ ///
+ /// Opens the URL with the given string.
+ /// The browser and mode used is based on what's configured in Flow's default browser settings.
+ /// Non-C# plugins should use this method.
+ ///
+ public void OpenUrl(string url, bool? inPrivate = null)
+ {
+ _api.OpenUrl(url, inPrivate);
+ }
+
+
+ ///
+ /// Opens the application URI with the given string, e.g. obsidian://search-query-example
+ /// Non-C# plugins should use this method
+ ///
+ public void OpenAppUri(string appUri)
+ {
+ _api.OpenAppUri(appUri);
+ }
+ }
+}
diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
index c3b47a79c..59bb0bd84 100644
--- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
@@ -6,6 +6,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
+using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using Microsoft.VisualStudio.Threading;
@@ -13,14 +14,14 @@ using StreamJsonRpc;
namespace Flow.Launcher.Core.Plugin
{
- internal class PythonPluginV2 : JsonRpcPluginV2
+ internal class PythonPluginV2 : JsonRPCPluginV2, IReloadable, IDisposable
{
private readonly ProcessStartInfo _startInfo;
private Process _process;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Python;
- protected override JsonRpc Rpc { get; set; }
+ protected override JsonRpc RPC { get; set; }
public PythonPluginV2(string filename)
@@ -53,7 +54,7 @@ namespace Flow.Launcher.Core.Plugin
{
throw new NotImplementedException();
}
-
+
public override async Task InitAsync(PluginInitContext context)
{
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
@@ -63,17 +64,44 @@ namespace Flow.Launcher.Core.Plugin
ArgumentNullException.ThrowIfNull(_process);
- var formatter = new JsonMessageFormatter();
- var handler = new NewLineDelimitedMessageHandler(_process.StandardInput.BaseStream,
- _process.StandardOutput.BaseStream,
- formatter);
-
- Rpc = new JsonRpc(handler, context.API);
- Rpc.StartListening();
-
- _ = _process.StandardError.ReadToEndAsync().ContinueWith(e => throw new Exception(e.Result));
+ SetupJsonRPC(_process, context.API);
await base.InitAsync(context);
}
+
+ public void Dispose()
+ {
+ _process.Kill(true);
+ _process.Dispose();
+ base.Dispose();
+ }
+
+ public void ReloadData()
+ {
+ var oldProcess = _process;
+ _process = Process.Start(_startInfo);
+ ArgumentNullException.ThrowIfNull(_process);
+ SetupJsonRPC(_process, Context.API);
+ oldProcess.Kill(true);
+ oldProcess.Dispose();
+ }
+
+ private void SetupJsonRPC(Process process, IPublicAPI api)
+ {
+ var formatter = new JsonMessageFormatter();
+ var handler = new NewLineDelimitedMessageHandler(process.StandardInput.BaseStream,
+ process.StandardOutput.BaseStream,
+ formatter);
+
+ RPC = new JsonRpc(handler, new JsonRPCPublicAPI(api));
+ RPC.SynchronizationContext = null;
+ RPC.StartListening();
+
+ _ = process.StandardError.ReadToEndAsync().ContinueWith(e =>
+ {
+ if (e.Result.Length > 0)
+ throw new Exception(e.Result);
+ });
+ }
}
}