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