diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 0c139f521b0..40eb1be3eae 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -58,7 +58,7 @@ internal IEnumerable Setup() string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) { var msg = $"Please select the {EnvName} executable"; - var selectedFile = string.Empty; + string selectedFile; selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -131,14 +131,8 @@ private string GetFileFromDialog(string title, string filter = "") }; var result = dlg.ShowDialog(); - if (result == DialogResult.OK) - { - return dlg.FileName; - } - else - { - return string.Empty; - } + return result == DialogResult.OK ? dlg.FileName : string.Empty; + } /// diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs new file mode 100644 index 00000000000..6c8c5aa57c8 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + + internal class JavaScriptV2Environment : TypeScriptV2Environment + { + internal override string Language => AllowedLanguage.JavaScriptV2; + + internal JavaScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs new file mode 100644 index 00000000000..4d75e1b8f00 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class PythonV2Environment : PythonEnvironment + { + internal override string Language => AllowedLanguage.PythonV2; + + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new PythonPluginV2(filePath), + Metadata = metadata + }; + } + + internal PythonV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs new file mode 100644 index 00000000000..11ed94d3f4d --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Droplex; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin; +using System.IO; +using Flow.Launcher.Core.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class TypeScriptV2Environment : AbstractPluginEnvironment + { + internal override string Language => AllowedLanguage.TypeScriptV2; + + internal override string EnvName => DataLocation.NodeEnvironmentName; + + internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName); + + internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); + internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); + + internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } + + internal TypeScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + + internal override void InstallEnvironment() + { + FilesFolders.RemoveFolderIfExists(InstallPath); + + DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); + + PluginsSettingsFilePath = ExecutablePath; + } + + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new NodePluginV2(filePath), + Metadata = metadata + }; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index c4dcef3e394..63f21c1d62b 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; using System.Threading; diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 39474c3c5c1..42f233dad3d 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -57,6 +57,7 @@ + diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index a7bbccfec5e..857122aa6f0 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -27,14 +28,14 @@ public ExecutablePlugin(string filename) protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = request.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = rpcRequest.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } } diff --git a/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs new file mode 100644 index 00000000000..ee1b315c2ea --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Core.Plugin +{ + internal sealed class ExecutablePluginV2 : ProcessStreamPluginV2 + { + protected override ProcessStartInfo StartInfo { get; set; } + + public ExecutablePluginV2(string filename) + { + StartInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + } + + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index eaaae25b0b9..48606eea465 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -19,67 +19,35 @@ namespace Flow.Launcher.Core.Plugin { - public class JsonRPCErrorModel - { - public int Code { get; set; } - - public string Message { get; set; } - - public string Data { get; set; } - } - - - public class JsonRPCResponseModel - { - public string Result { get; set; } + public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default); + public record JsonRPCErrorModel(int Code, string Message, string Data); - public JsonRPCErrorModel Error { get; set; } - } - - public class JsonRPCQueryResponseModel : JsonRPCResponseModel - { - [JsonPropertyName("result")] - public new List Result { get; set; } + public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); + public record JsonRPCQueryResponseModel(int Id, + [property: JsonPropertyName("result")] List Result, + IReadOnlyDictionary SettingsChanges = null, + string DebugMessage = "", + JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error); - public Dictionary SettingsChange { get; set; } + public record JsonRPCRequestModel(int Id, + string Method, + object[] Parameters, + IReadOnlyDictionary Settings = default, + JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); - public string DebugMessage { get; set; } - } - - public class JsonRPCRequestModel - { - public string Method { get; set; } - - public object[] Parameters { get; set; } - - public Dictionary Settings { get; set; } - - private static readonly JsonSerializerOptions options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - public override string ToString() - { - return JsonSerializer.Serialize(this, options); - } - } - - /// - /// Json RPC Request that Flow Launcher sent to client - /// - public class JsonRPCServerRequestModel : JsonRPCRequestModel - { - - } /// /// Json RPC Request(in query response) that client sent to Flow Launcher /// - public class JsonRPCClientRequestModel : JsonRPCRequestModel - { - public bool DontHideAfterAction { get; set; } - } - + public record JsonRPCClientRequestModel( + int Id, + string Method, + object[] Parameters, + IReadOnlyDictionary Settings, + bool DontHideAfterAction = false, + JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error); + + /// /// Represent the json-rpc result item that client send to Flow Launcher /// Typically, we will send back this request model to client after user select the result item diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 438c1dd8a8a..97c3c898121 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -30,9 +30,8 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRPCPlugin : JsonRPCPluginBase { - protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); @@ -40,19 +39,16 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProv private static readonly RecyclableMemoryStreamManager BufferManager = new(); - private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json"); + private int RequestId { get; set; } - public List LoadContextMenus(Result selectedResult) + private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + + public override List LoadContextMenus(Result selectedResult) { - var request = new JsonRPCRequestModel - { - Method = "context_menu", - Parameters = new[] - { - selectedResult.ContextData - } - }; + var request = new JsonRPCRequestModel(RequestId++, + "context_menu", + new[] { selectedResult.ContextData }); var output = Request(request); return DeserializedResult(output); } @@ -77,7 +73,6 @@ public List LoadContextMenus(Result selectedResult) { WriteIndented = true }; - private Dictionary Settings { get; set; } private readonly Dictionary _settingControls = new(); @@ -103,85 +98,42 @@ private List DeserializedResult(string output) return ParseResults(queryResponseModel); } - - private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + protected override async Task ExecuteResultAsync(JsonRPCResult result) { - if (queryResponseModel.Result == null) return null; + if (result.JsonRPCAction == null) return false; - if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) { - context.API.ShowMsg(queryResponseModel.DebugMessage); + return !result.JsonRPCAction.DontHideAfterAction; } - foreach (var result in queryResponseModel.Result) + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - result.AsyncAction = async c => - { - UpdateSettings(result.SettingsChange); - - if (result.JsonRPCAction == null) return false; - - if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], - result.JsonRPCAction.Parameters); - } - else - { - await using var actionResponse = await RequestAsync(result.JsonRPCAction); - - if (actionResponse.Length == 0) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - var jsonRpcRequestModel = await - JsonSerializer.DeserializeAsync(actionResponse, options); - - if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], - jsonRpcRequestModel.Parameters); - } - } - - return !result.JsonRPCAction.DontHideAfterAction; - }; + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); } + else + { + await using var actionResponse = await RequestAsync(result.JsonRPCAction); - var results = new List(); + if (actionResponse.Length == 0) + { + return !result.JsonRPCAction.DontHideAfterAction; + } - results.AddRange(queryResponseModel.Result); + var jsonRpcRequestModel = await + JsonSerializer.DeserializeAsync(actionResponse, options); - UpdateSettings(queryResponseModel.SettingsChange); + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) + { + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); + } + } - return results; + return !result.JsonRPCAction.DontHideAfterAction; } - private void ExecuteFlowLauncherAPI(string method, object[] parameters) - { - var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); - var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray); - if (methodInfo == null) - { - return; - } - try - { - methodInfo.Invoke(PluginManager.API, parameters); - } - catch (Exception) - { -#if (DEBUG) - throw; -#endif - } - } /// /// Execute external program and return the output @@ -296,379 +248,19 @@ protected async Task ExecuteAsync(ProcessStartInfo startInfo, Cancellati return sourceBuffer; } - - public async Task> QueryAsync(Query query, CancellationToken token) + public override async Task> QueryAsync(Query query, CancellationToken token) { - var request = new JsonRPCRequestModel - { - Method = "query", - Parameters = new object[] + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[] { query.Search }, - Settings = Settings - }; - var output = await RequestAsync(request, token); - return await DeserializedResultAsync(output); - } - - public async Task InitSettingAsync() - { - if (!File.Exists(SettingConfigurationPath)) - return; - - if (File.Exists(SettingPath)) - { - await using var fileStream = File.OpenRead(SettingPath); - Settings = await JsonSerializer.DeserializeAsync>(fileStream, options); - } - - var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); - _settingsTemplate = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); - - Settings ??= new Dictionary(); - - foreach (var (type, attribute) in _settingsTemplate.Body) - { - if (type == "textBlock") - continue; - if (!Settings.ContainsKey(attribute.Name)) - { - Settings[attribute.Name] = attribute.DefaultValue; - } - } - } - - public virtual async Task InitAsync(PluginInitContext context) - { - this.context = context; - await InitSettingAsync(); - } - private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); - private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); - private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); - private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); - private JsonRpcConfigurationModel _settingsTemplate; - - public Control CreateSettingPanel() - { - if (Settings == null) - return new(); - var settingWindow = new UserControl(); - var mainPanel = new Grid - { - Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center - }; - ColumnDefinition gridCol1 = new ColumnDefinition(); - ColumnDefinition gridCol2 = new ColumnDefinition(); - - gridCol1.Width = new GridLength(70, GridUnitType.Star); - gridCol2.Width = new GridLength(30, GridUnitType.Star); - mainPanel.ColumnDefinitions.Add(gridCol1); - mainPanel.ColumnDefinitions.Add(gridCol2); - settingWindow.Content = mainPanel; - int rowCount = 0; - foreach (var (type, attribute) in _settingsTemplate.Body) - { - Separator sep = new Separator(); - sep.VerticalAlignment = VerticalAlignment.Top; - sep.Margin = settingSepMargin; - sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ - var panel = new StackPanel - { - Orientation = Orientation.Vertical, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelPanelMargin - }; - RowDefinition gridRow = new RowDefinition(); - mainPanel.RowDefinitions.Add(gridRow); - var name = new TextBlock() - { - Text = attribute.Label, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - var desc = new TextBlock() - { - Text = attribute.Description, - FontSize = 12, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingDescMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); - - if (attribute.Description == null) /* if no description, hide */ - desc.Visibility = Visibility.Collapsed; - - - if (type != "textBlock") /* if textBlock, hide desc */ - { - panel.Children.Add(name); - panel.Children.Add(desc); - } - - - Grid.SetColumn(panel, 0); - Grid.SetRow(panel, rowCount); + Settings?.Inner); - FrameworkElement contentControl; - - switch (type) - { - case "textBlock": - { - contentControl = new TextBlock - { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - Padding = new Thickness(0, 0, 0, 0), - HorizontalAlignment = System.Windows.HorizontalAlignment.Left, - TextAlignment = TextAlignment.Left, - TextWrapping = TextWrapping.Wrap - }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "input": - { - var textBox = new TextBox() - { - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "inputWithFileBtn": - { - var textBox = new TextBox() - { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10, 0, 0, 0), Content = "Browse" - }; - var dockPanel = new DockPanel() - { - Margin = settingControlMargin - }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "textarea": - { - var textBox = new TextBox() - { - Height = 120, - Margin = settingControlMargin, - VerticalAlignment = VerticalAlignment.Center, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "passwordBox": - { - var passwordBox = new PasswordBox() - { - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; - contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "dropdown": - { - var comboBox = new System.Windows.Controls.ComboBox() - { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; - }; - contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "checkbox": - var checkBox = new CheckBox - { - IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), - Margin = settingCheckboxMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - checkBox.Click += (sender, _) => - { - Settings[attribute.Name] = ((CheckBox)sender).IsChecked; - }; - contentControl = checkBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - case "hyperlink": - var hyperlink = new Hyperlink - { - ToolTip = attribute.Description, NavigateUri = attribute.url - }; - var linkbtn = new System.Windows.Controls.Button - { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin - }; - linkbtn.Content = attribute.urlLabel; - - contentControl = linkbtn; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - default: - continue; - } - if (type != "textBlock") - _settingControls[attribute.Name] = contentControl; - mainPanel.Children.Add(panel); - mainPanel.Children.Add(contentControl); - rowCount++; - - } - return settingWindow; - } - - public void Save() - { - if (Settings != null) - { - Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name)); - File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption)); - } - } - - public void UpdateSettings(Dictionary settings) - { - if (settings == null || settings.Count == 0) - return; - - foreach (var (key, value) in settings) - { - if (Settings.ContainsKey(key)) - { - Settings[key] = value; - } - if (_settingControls.ContainsKey(key)) - { + var output = await RequestAsync(request, token); - switch (_settingControls[key]) - { - case TextBox textBox: - textBox.Dispatcher.Invoke(() => textBox.Text = value as string); - break; - case PasswordBox passwordBox: - passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string); - break; - case System.Windows.Controls.ComboBox comboBox: - comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); - break; - case CheckBox checkBox: - checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string)); - break; - } - } - } + return await DeserializedResultAsync(output); } } - } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs new file mode 100644 index 00000000000..330120c1286 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -0,0 +1,162 @@ +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Infrastructure; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Microsoft.IO; +using System.Windows; +using System.Windows.Controls; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using CheckBox = System.Windows.Controls.CheckBox; +using Control = System.Windows.Controls.Control; +using Orientation = System.Windows.Controls.Orientation; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; +using System.Windows.Documents; +using static System.Windows.Forms.LinkLabel; +using Droplex; +using System.Windows.Forms; +using Microsoft.VisualStudio.Threading; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Represent the plugin that using JsonPRC + /// every JsonRPC plugin should has its own plugin instance + /// + internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + { + protected PluginInitContext Context; + public const string JsonRPC = "JsonRPC"; + + private int RequestId { get; set; } + + private string SettingConfigurationPath => + Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, + Context.CurrentPluginMetadata.Name, "Settings.json"); + + public abstract List LoadContextMenus(Result selectedResult); + + protected static readonly JsonSerializerOptions DeserializeOption = new() + { + PropertyNameCaseInsensitive = true, +#pragma warning disable SYSLIB0020 + // IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still + // deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour + // to accept null and fallback to a default etc, or just keep IgnoreNullValues for now + // see: https://github.com/dotnet/runtime/issues/39152 + IgnoreNullValues = true, +#pragma warning restore SYSLIB0020 // Type or member is obsolete + Converters = { new JsonObjectConverter() } + }; + + protected static readonly JsonSerializerOptions RequestSerializeOption = new() + { + PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + protected abstract Task ExecuteResultAsync(JsonRPCResult result); + + protected JsonRPCPluginSettings Settings { get; set; } + + protected List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + { + if (queryResponseModel.Result == null) return null; + + if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + { + Context.API.ShowMsg(queryResponseModel.DebugMessage); + } + + foreach (var result in queryResponseModel.Result) + { + result.AsyncAction = async _ => + { + Settings?.UpdateSettings(result.SettingsChange); + + return await ExecuteResultAsync(result); + }; + } + + var results = new List(); + + results.AddRange(queryResponseModel.Result); + + Settings?.UpdateSettings(queryResponseModel.SettingsChanges); + + return results; + } + + protected void ExecuteFlowLauncherAPI(string method, object[] parameters) + { + var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); + var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray); + + if (methodInfo == null) + { + return; + } + + try + { + methodInfo.Invoke(Context.API, parameters); + } + catch (Exception) + { +#if (DEBUG) + throw; +#endif + } + } + + public abstract Task> QueryAsync(Query query, CancellationToken token); + + + private async Task InitSettingAsync() + { + if (!File.Exists(SettingConfigurationPath)) + return; + + var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + var configuration = + deserializer.Deserialize( + await File.ReadAllTextAsync(SettingConfigurationPath)); + + Settings ??= new JsonRPCPluginSettings + { + Configuration = configuration, SettingPath = SettingPath, API = Context.API + }; + + await Settings.InitializeAsync(); + } + + public virtual async Task InitAsync(PluginInitContext context) + { + this.Context = context; + await InitSettingAsync(); + } + + public void Save() + { + Settings?.Save(); + } + + public Control CreateSettingPanel() + { + return Settings.CreateSettingPanel(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs new file mode 100644 index 00000000000..b87623c5676 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -0,0 +1,415 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + public class JsonRPCPluginSettings + { + public required JsonRpcConfigurationModel Configuration { get; init; } + + public required string SettingPath { get; init; } + public Dictionary SettingControls { get; } = new(); + + public IReadOnlyDictionary Inner => Settings; + protected Dictionary Settings { get; set; } + public required IPublicAPI API { get; init; } + + private JsonStorage> _storage; + + // maybe move to resource? + private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); + private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); + private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); + private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); + + public async Task InitializeAsync() + { + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + + foreach (var (type, attributes) in Configuration.Body) + { + if (attributes.Name == null) + { + continue; + } + + if (!Settings.ContainsKey(attributes.Name)) + { + Settings[attributes.Name] = attributes.DefaultValue; + } + } + } + + + public void UpdateSettings(IReadOnlyDictionary settings) + { + if (settings == null || settings.Count == 0) + return; + + foreach (var (key, value) in settings) + { + if (Settings.ContainsKey(key)) + { + Settings[key] = value; + } + + if (SettingControls.TryGetValue(key, out var control)) + { + switch (control) + { + case TextBox textBox: + textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty); + break; + case PasswordBox passwordBox: + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty); + break; + case ComboBox comboBox: + comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); + break; + case CheckBox checkBox: + checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string ?? string.Empty)); + break; + } + } + } + } + + public async Task SaveAsync() + { + await _storage.SaveAsync(); + } + + public void Save() + { + _storage.Save(); + } + + public Control CreateSettingPanel() + { + if (Settings == null) + return new(); + + var settingWindow = new UserControl(); + var mainPanel = new Grid + { + Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center + }; + + ColumnDefinition gridCol1 = new ColumnDefinition(); + ColumnDefinition gridCol2 = new ColumnDefinition(); + + gridCol1.Width = new GridLength(70, GridUnitType.Star); + gridCol2.Width = new GridLength(30, GridUnitType.Star); + mainPanel.ColumnDefinitions.Add(gridCol1); + mainPanel.ColumnDefinitions.Add(gridCol2); + settingWindow.Content = mainPanel; + int rowCount = 0; + + foreach (var (type, attribute) in Configuration.Body) + { + Separator sep = new Separator(); + sep.VerticalAlignment = VerticalAlignment.Top; + sep.Margin = settingSepMargin; + sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ + var panel = new StackPanel + { + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelPanelMargin + }; + + RowDefinition gridRow = new RowDefinition(); + mainPanel.RowDefinitions.Add(gridRow); + var name = new TextBlock() + { + Text = attribute.Label, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + var desc = new TextBlock() + { + Text = attribute.Description, + FontSize = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingDescMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); + + if (attribute.Description == null) /* if no description, hide */ + desc.Visibility = Visibility.Collapsed; + + + if (type != "textBlock") /* if textBlock, hide desc */ + { + panel.Children.Add(name); + panel.Children.Add(desc); + } + + + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + FrameworkElement contentControl; + + switch (type) + { + case "textBlock": + { + contentControl = new TextBlock + { + Text = attribute.Description.Replace("\\r\\n", "\r\n"), + Margin = settingTextBlockMargin, + Padding = new Thickness(0, 0, 0, 0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "input": + { + var textBox = new TextBox() + { + Text = Settings[attribute.Name] as string ?? string.Empty, + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "inputWithFileBtn": + { + var textBox = new TextBox() + { + Margin = new Thickness(10, 0, 0, 0), + Text = Settings[attribute.Name] as string ?? string.Empty, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + var Btn = new System.Windows.Controls.Button() + { + Margin = new Thickness(10, 0, 0, 0), Content = "Browse" + }; + + var dockPanel = new DockPanel() + { + Margin = settingControlMargin + }; + + DockPanel.SetDock(Btn, Dock.Right); + dockPanel.Children.Add(Btn); + dockPanel.Children.Add(textBox); + contentControl = dockPanel; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "textarea": + { + var textBox = new TextBox() + { + Height = 120, + Margin = settingControlMargin, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + Text = Settings[attribute.Name] as string ?? string.Empty, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (sender, _) => + { + Settings[attribute.Name] = ((TextBox)sender).Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "passwordBox": + { + var passwordBox = new PasswordBox() + { + Margin = settingControlMargin, + Password = Settings[attribute.Name] as string ?? string.Empty, + PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attribute.Name] = ((PasswordBox)sender).Password; + }; + + contentControl = passwordBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "dropdown": + { + var comboBox = new System.Windows.Controls.ComboBox() + { + ItemsSource = attribute.Options, + SelectedItem = Settings[attribute.Name], + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + comboBox.SelectionChanged += (sender, _) => + { + Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; + }; + + contentControl = comboBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "checkbox": + var checkBox = new CheckBox + { + IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), + Margin = settingCheckboxMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + checkBox.Click += (sender, _) => + { + Settings[attribute.Name] = ((CheckBox)sender).IsChecked; + }; + + contentControl = checkBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + case "hyperlink": + var hyperlink = new Hyperlink + { + ToolTip = attribute.Description, NavigateUri = attribute.url + }; + + var linkbtn = new System.Windows.Controls.Button + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin + }; + + linkbtn.Content = attribute.urlLabel; + + contentControl = linkbtn; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + default: + continue; + } + + if (type != "textBlock") + SettingControls[attribute.Name] = contentControl; + + mainPanel.Children.Add(panel); + mainPanel.Children.Add(contentControl); + rowCount++; + + } + + return settingWindow; + } + + + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs new file mode 100644 index 00000000000..305ba9b6536 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Core.Plugin.JsonRPCV2Models; +using Flow.Launcher.Plugin; +using Microsoft.VisualStudio.Threading; +using StreamJsonRpc; +using IAsyncDisposable = System.IAsyncDisposable; + + +namespace Flow.Launcher.Core.Plugin +{ + internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated + { + public abstract string SupportedLanguage { get; set; } + + public const string JsonRpc = "JsonRPC"; + + protected abstract IDuplexPipe ClientPipe { get; set; } + + protected StreamReader ErrorStream { get; set; } + + private JsonRpc RPC { get; set; } + + + protected override async Task ExecuteResultAsync(JsonRPCResult result) + { + try + { + var res = await RPC.InvokeAsync(result.JsonRPCAction.Method, + argument: result.JsonRPCAction.Parameters); + + return res.Hide; + } + catch + { + return false; + } + } + + public override List LoadContextMenus(Result selectedResult) + { + throw new NotImplementedException(); + } + + public override async Task> QueryAsync(Query query, CancellationToken token) + { + try + { + var res = await RPC.InvokeWithCancellationAsync("query", + new[] { query }, + token); + + var results = ParseResults(res); + + return results; + } + catch + { + return new List(); + } + } + + + public override async Task InitAsync(PluginInitContext context) + { + await base.InitAsync(context); + + SetupJsonRPC(); + + _ = ReadErrorAsync(); + + await RPC.InvokeAsync("initialize", context); + + async Task ReadErrorAsync() + { + var error = await ErrorStream.ReadToEndAsync(); + + if (!string.IsNullOrEmpty(error)) + { + throw new Exception(error); + } + } + } + + public event ResultUpdatedEventHandler ResultsUpdated; + + + private void SetupJsonRPC() + { + var formatter = new SystemTextJsonFormatter(); + var handler = new NewLineDelimitedMessageHandler(ClientPipe, + formatter); + + RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API)); + + RPC.AddLocalRpcMethod("UpdateResults", new Action((rawQuery, response) => + { + var results = ParseResults(response); + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Query = new Query() + { + RawQuery = rawQuery + }, Results = results }); + })); + RPC.SynchronizationContext = null; + RPC.StartListening(); + } + + public virtual Task ReloadDataAsync() + { + SetupJsonRPC(); + return Task.CompletedTask; + } + + public virtual ValueTask DisposeAsync() + { + RPC?.Dispose(); + ErrorStream?.Dispose(); + return ValueTask.CompletedTask; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs new file mode 100644 index 00000000000..6a130f70fdd --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs @@ -0,0 +1,4 @@ +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public 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 00000000000..b8bfee591e6 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -0,0 +1,177 @@ +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; + } + + public void ChangeQuery(string query, bool requery = false) + { + _api.ChangeQuery(query, requery); + } + + public void RestartApp() + { + _api.RestartApp(); + } + + public void ShellRun(string cmd, string filename = "cmd.exe") + { + _api.ShellRun(cmd, filename); + } + + public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) + { + _api.CopyToClipboard(text, directCopy, showDefaultNotification); + } + + public void SaveAppAllSettings() + { + _api.SaveAppAllSettings(); + } + + public void SavePluginSettings() + { + _api.SavePluginSettings(); + } + + public Task ReloadAllPluginDataAsync() + { + return _api.ReloadAllPluginData(); + } + + public void CheckForNewUpdate() + { + _api.CheckForNewUpdate(); + } + + public void ShowMsgError(string title, string subTitle = "") + { + _api.ShowMsgError(title, subTitle); + } + + public void ShowMainWindow() + { + _api.ShowMainWindow(); + } + + public void HideMainWindow() + { + _api.HideMainWindow(); + } + + public bool IsMainWindowVisible() + { + return _api.IsMainWindowVisible(); + } + + public void ShowMsg(string title, string subTitle = "", string iconPath = "") + { + _api.ShowMsg(title, subTitle, iconPath); + } + + public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) + { + _api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner); + } + + public void OpenSettingDialog() + { + _api.OpenSettingDialog(); + } + + public string GetTranslation(string key) + { + return _api.GetTranslation(key); + } + + public List GetAllPlugins() + { + return _api.GetAllPlugins(); + } + + + public MatchResult FuzzySearch(string query, string stringToCompare) + { + return _api.FuzzySearch(query, stringToCompare); + } + + public Task HttpGetStringAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStringAsync(url, token); + } + + public Task HttpGetStreamAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStreamAsync(url, token); + } + + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, + CancellationToken token = default) + { + return _api.HttpDownloadAsync(url, filePath, token); + } + + public void AddActionKeyword(string pluginId, string newActionKeyword) + { + _api.AddActionKeyword(pluginId, newActionKeyword); + } + + public void RemoveActionKeyword(string pluginId, string oldActionKeyword) + { + _api.RemoveActionKeyword(pluginId, oldActionKeyword); + } + + public bool ActionKeywordAssigned(string actionKeyword) + { + return _api.ActionKeywordAssigned(actionKeyword); + } + + public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogDebug(className, message, methodName); + } + + public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogInfo(className, message, methodName); + } + + public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogWarn(className, message, methodName); + } + + public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) + { + _api.OpenDirectory(DirectoryPath, FileNameOrFilePath); + } + + + public void OpenUrl(string url, bool? inPrivate = null) + { + _api.OpenUrl(url, inPrivate); + } + + + public void OpenAppUri(string appUri) + { + _api.OpenAppUri(appUri); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs new file mode 100644 index 00000000000..003724a235f --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public record JsonRPCQueryRequest( + List Results + ); +} diff --git a/Flow.Launcher.Core/Plugin/NodePlugin.cs b/Flow.Launcher.Core/Plugin/NodePlugin.cs index 8ea5c4b785a..40eb057cbc7 100644 --- a/Flow.Launcher.Core/Plugin/NodePlugin.cs +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin; @@ -27,23 +28,22 @@ public NodePlugin(string filename) protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[1] = request.ToString(); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(request); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[1] = rpcRequest.ToString(); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest); return Execute(_startInfo); } public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.ArgumentList.Add(string.Empty); - await base.InitAsync(context); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + await base.InitAsync(context); } } } diff --git a/Flow.Launcher.Core/Plugin/NodePluginV2.cs b/Flow.Launcher.Core/Plugin/NodePluginV2.cs new file mode 100644 index 00000000000..6c95777f055 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/NodePluginV2.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Execution of JavaScript & TypeScript plugins + /// + internal class NodePluginV2 : ProcessStreamPluginV2 + { + public NodePluginV2(string filename) + { + StartInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + } + + public override string SupportedLanguage { get; set; } + protected override ProcessStartInfo StartInfo { get; set; } + + public override async Task InitAsync(PluginInitContext context) + { + StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + StartInfo.ArgumentList.Add(string.Empty); + StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + await base.InitAsync(context); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index fea7f55b9dc..0f2e4f996cb 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -19,22 +19,33 @@ public static class PluginsLoader public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); - + var pythonEnv = new PythonEnvironment(metadatas, settings); + var pythonV2Env = new PythonV2Environment(metadatas, settings); var tsEnv = new TypeScriptEnvironment(metadatas, settings); var jsEnv = new JavaScriptEnvironment(metadatas, settings); + var tsV2Env = new TypeScriptV2Environment(metadatas, settings); + var jsV2Env = new JavaScriptV2Environment(metadatas, settings); var pythonPlugins = pythonEnv.Setup(); + var pythonV2Plugins = pythonV2Env.Setup(); var tsPlugins = tsEnv.Setup(); var jsPlugins = jsEnv.Setup(); - + var tsV2Plugins = tsV2Env.Setup(); + var jsV2Plugins = jsV2Env.Setup(); + var executablePlugins = ExecutablePlugins(metadatas); - + var executableV2Plugins = ExecutableV2Plugins(metadatas); + var plugins = dotnetPlugins - .Concat(pythonPlugins) - .Concat(tsPlugins) - .Concat(jsPlugins) - .Concat(executablePlugins) - .ToList(); + .Concat(pythonPlugins) + .Concat(pythonV2Plugins) + .Concat(tsPlugins) + .Concat(jsPlugins) + .Concat(tsV2Plugins) + .Concat(jsV2Plugins) + .Concat(executablePlugins) + .Concat(executableV2Plugins) + .ToList(); return plugins; } @@ -93,7 +104,7 @@ public static IEnumerable DotNetPlugins(List source) return; } - plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); }); metadata.InitTime += milliseconds; } @@ -118,7 +129,7 @@ public static IEnumerable DotNetPlugins(List source) return plugins; } - public static IEnumerable ExecutablePlugins(IEnumerable source) + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) @@ -127,5 +138,15 @@ public static IEnumerable ExecutablePlugins(IEnumerable ExecutableV2Plugins(IEnumerable source) + { + return source + .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) + .Select(metadata => new PluginPair + { + Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata + }); + } } } diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs new file mode 100644 index 00000000000..24d06d9756c --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin; +using Nerdbank.Streams; + +namespace Flow.Launcher.Core.Plugin +{ + internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2 + { + + public override string SupportedLanguage { get; set; } + protected sealed override IDuplexPipe ClientPipe { get; set; } + + protected abstract ProcessStartInfo StartInfo { get; set; } + + public Process ClientProcess { get; set; } + + public override async Task InitAsync(PluginInitContext context) + { + StartInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; + StartInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; + StartInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; + + StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + + ClientProcess = Process.Start(StartInfo); + ArgumentNullException.ThrowIfNull(ClientProcess); + + SetupPipe(ClientProcess); + + await base.InitAsync(context); + } + + private void SetupPipe(Process process) + { + var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream), + PipeWriter.Create(process.StandardInput.BaseStream)); + ClientPipe = new DuplexPipe(reader, writer); + } + + + public override async Task ReloadDataAsync() + { + var oldProcess = ClientProcess; + ClientProcess = Process.Start(StartInfo); + ArgumentNullException.ThrowIfNull(ClientProcess); + SetupPipe(ClientProcess); + await base.ReloadDataAsync(); + oldProcess.Kill(true); + await oldProcess.WaitForExitAsync(); + oldProcess.Dispose(); + } + + + public override async ValueTask DisposeAsync() + { + ClientProcess.Kill(true); + await ClientProcess.WaitForExitAsync(); + ClientProcess.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index d8df2122682..536e69b3dbb 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; @@ -22,7 +23,6 @@ public PythonPlugin(string filename) RedirectStandardError = true, }; - // temp fix for issue #667 var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; @@ -37,7 +37,7 @@ public PythonPlugin(string filename) protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[2] = request.ToString(); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } @@ -45,8 +45,8 @@ protected override Task RequestAsync(JsonRPCRequestModel request, Cancel protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[2] = rpcRequest.ToString(); - _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); + _startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs new file mode 100644 index 00000000000..4a8d8d7def3 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +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; +using Nerdbank.Streams; +using StreamJsonRpc; + +namespace Flow.Launcher.Core.Plugin +{ + internal sealed class PythonPluginV2 : ProcessStreamPluginV2 + { + public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; + protected override ProcessStartInfo StartInfo { get; set; } + + public PythonPluginV2(string filename) + { + StartInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true + }; + + 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"); + } + } +} diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 45456ddebdc..64225062785 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure.Storage @@ -26,6 +27,85 @@ namespace Flow.Launcher.Infrastructure.Storage protected string DirectoryPath { get; init; } = null!; + // Let the derived class to set the file path + protected JsonStorage() + { + } + public JsonStorage(string filePath) + { + FilePath = filePath; + DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); + + Helper.ValidateDirectory(DirectoryPath); + } + + public async Task LoadAsync() + { + if (Data != null) + return Data; + + string? serialized = null; + + if (File.Exists(FilePath)) + { + serialized = await File.ReadAllTextAsync(FilePath); + } + + if (!string.IsNullOrEmpty(serialized)) + { + try + { + Data = JsonSerializer.Deserialize(serialized) ?? await LoadBackupOrDefaultAsync(); + } + catch (JsonException) + { + Data = await LoadBackupOrDefaultAsync(); + } + } + else + { + Data = await LoadBackupOrDefaultAsync(); + } + + return Data.NonNull(); + } + + private async ValueTask LoadBackupOrDefaultAsync() + { + var backup = await TryLoadBackupAsync(); + + return backup ?? LoadDefault(); + } + + private async ValueTask TryLoadBackupAsync() + { + if (!File.Exists(BackupFilePath)) + return default; + + try + { + await using var source = File.OpenRead(BackupFilePath); + var data = await JsonSerializer.DeserializeAsync(source) ?? default; + + if (data != null) + RestoreBackup(); + + return data; + } + catch (JsonException) + { + return default; + } + } + private void RestoreBackup() + { + Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); + + if (File.Exists(FilePath)) + File.Replace(BackupFilePath, FilePath, null); + else + File.Move(BackupFilePath, FilePath); + } public T Load() { @@ -75,18 +155,9 @@ private T LoadDefault() var data = JsonSerializer.Deserialize(File.ReadAllText(BackupFilePath)); if (data != null) - { - Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); - - if(File.Exists(FilePath)) - File.Replace(BackupFilePath, FilePath, null); - else - File.Move(BackupFilePath, FilePath); - - return data; - } + RestoreBackup(); - return default; + return data; } catch (JsonException) { @@ -115,6 +186,20 @@ public void Save() File.WriteAllText(TempFilePath, serialized); + AtomicWriteSetting(); + } + public async Task SaveAsync() + { + var tempOutput = File.OpenWrite(TempFilePath); + await JsonSerializer.SerializeAsync(tempOutput, Data, + new JsonSerializerOptions + { + WriteIndented = true + }); + AtomicWriteSetting(); + } + private void AtomicWriteSetting() + { if (!File.Exists(FilePath)) { File.Move(TempFilePath, FilePath); @@ -124,5 +209,6 @@ public void Save() File.Replace(TempFilePath, FilePath, BackupFilePath); } } + } } diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index d5eea4fa56b..619a94deb50 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -12,6 +12,11 @@ public static class AllowedLanguage /// public const string Python = "Python"; + /// + /// Python V2 + /// + public const string PythonV2 = "Python_v2"; + /// /// C# /// @@ -27,16 +32,31 @@ public static class AllowedLanguage /// public const string Executable = "Executable"; + /// + /// Standard .exe + /// + public const string ExecutableV2 = "Executable_V2"; + /// /// TypeScript /// public const string TypeScript = "TypeScript"; + /// + /// TypeScript + /// + public const string TypeScriptV2 = "TypeScript_V2"; + /// /// JavaScript /// public const string JavaScript = "JavaScript"; + /// + /// JavaScript + /// + public const string JavaScriptV2 = "JavaScript_V2"; + /// /// Determines if this language is a .NET language /// @@ -45,7 +65,7 @@ public static class AllowedLanguage public static bool IsDotNet(string language) { return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) - || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); + || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); } /// @@ -56,10 +76,15 @@ public static bool IsDotNet(string language) public static bool IsAllowed(string language) { return IsDotNet(language) - || language.Equals(Python, StringComparison.OrdinalIgnoreCase) - || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) - || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) - || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase); + || language.Equals(Python, StringComparison.OrdinalIgnoreCase) + || language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase); + ; } } } diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 67228584008..b41675a1aa4 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { @@ -64,6 +67,7 @@ public Query(string rawQuery, string search, string[] terms, string[] searchTerm /// public string ActionKeyword { get; init; } + [JsonIgnore] /// /// Splits by spaces and returns the first item. /// @@ -71,12 +75,14 @@ public Query(string rawQuery, string search, string[] terms, string[] searchTerm /// returns an empty string when does not have enough items. /// public string FirstSearch => SplitSearch(0); - + + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// + [JsonIgnore] public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : ""; /// @@ -85,6 +91,7 @@ public Query(string rawQuery, string search, string[] terms, string[] searchTerm /// /// returns an empty string when does not have enough items. /// + [JsonIgnore] public string SecondSearch => SplitSearch(1); /// @@ -93,6 +100,7 @@ public Query(string rawQuery, string search, string[] terms, string[] searchTerm /// /// returns an empty string when does not have enough items. /// + [JsonIgnore] public string ThirdSearch => SplitSearch(2); private string SplitSearch(int index) diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index d071545ba77..3d05e56796f 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -53,21 +53,14 @@ public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullRe public static List ResponseModelsSource = new() { - new() + new JsonRPCQueryResponseModel(0, new List()), + new JsonRPCQueryResponseModel(0, new List { - Result = new() - }, - new() - { - Result = new() + new JsonRPCResult { - new JsonRPCResult - { - Title = "Test1", - SubTitle = "Test2" - } + Title = "Test1", SubTitle = "Test2" } - } + }) }; [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] @@ -94,4 +87,4 @@ public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSa } } -} \ No newline at end of file +}