diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
index 0c139f521..40eb1be3e 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
@@ -58,7 +58,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
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 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
};
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 000000000..6c8c5aa57
--- /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 000000000..4d75e1b8f
--- /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 000000000..11ed94d3f
--- /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 c4dcef3e3..63f21c1d6 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 39474c3c5..42f233dad 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 a7bbccfec..857122aa6 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 @@ namespace Flow.Launcher.Core.Plugin
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 000000000..ee1b315c2
--- /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 eaaae25b0..48606eea4 100644
--- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
+++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
@@ -19,67 +19,35 @@ using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
- public class JsonRPCErrorModel
- {
- public int Code { get; set; }
+ public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default);
+ public record JsonRPCErrorModel(int Code, string Message, string Data);
- public string Message { 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 string Data { get; set; }
- }
+ public record JsonRPCRequestModel(int Id,
+ string Method,
+ object[] Parameters,
+ IReadOnlyDictionary Settings = default,
+ JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
- public class JsonRPCResponseModel
- {
- public string Result { get; set; }
-
- public JsonRPCErrorModel Error { get; set; }
- }
-
- public class JsonRPCQueryResponseModel : JsonRPCResponseModel
- {
- [JsonPropertyName("result")]
- public new List Result { get; set; }
-
- public Dictionary SettingsChange { get; set; }
-
- 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 438c1dd8a..97c3c8981 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 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
{
WriteIndented = true
};
- private Dictionary Settings { get; set; }
private readonly Dictionary _settingControls = new();
@@ -103,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin
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 =>
+ ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
+ result.JsonRPCAction.Parameters);
+ }
+ else
+ {
+ await using var actionResponse = await RequestAsync(result.JsonRPCAction);
+
+ if (actionResponse.Length == 0)
{
- 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;
- };
+ }
+
+ var jsonRpcRequestModel = await
+ JsonSerializer.DeserializeAsync(actionResponse, options);
+
+ if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
+ {
+ ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
+ jsonRpcRequestModel.Parameters);
+ }
}
- var results = new List();
-
- results.AddRange(queryResponseModel.Result);
-
- UpdateSettings(queryResponseModel.SettingsChange);
-
- 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 @@ namespace Flow.Launcher.Core.Plugin
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
- };
+ Settings?.Inner);
+
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);
-
- 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))
- {
-
- 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;
- }
- }
- }
- }
}
-
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
new file mode 100644
index 000000000..330120c12
--- /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 000000000..b87623c56
--- /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 000000000..305ba9b65
--- /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 000000000..6a130f70f
--- /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 000000000..b8bfee591
--- /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 000000000..003724a23
--- /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 8ea5c4b78..40eb057cb 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 @@ namespace Flow.Launcher.Core.Plugin
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 000000000..6c95777f0
--- /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 fea7f55b9..0f2e4f996 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -19,22 +19,33 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
return;
}
- plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
+ plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
metadata.InitTime += milliseconds;
}
@@ -118,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
});
}
+
+ public static 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 000000000..24d06d975
--- /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 d8df21226..536e69b3d 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 @@ namespace Flow.Launcher.Core.Plugin
RedirectStandardError = true,
};
- // temp fix for issue #667
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
@@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
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 @@ namespace Flow.Launcher.Core.Plugin
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 000000000..4a8d8d7de
--- /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 45456ddeb..642250627 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -3,6 +3,7 @@ using System;
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 @@ namespace Flow.Launcher.Infrastructure.Storage
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);
+ RestoreBackup();
- return data;
- }
-
- return default;
+ return data;
}
catch (JsonException)
{
@@ -115,6 +186,20 @@ namespace Flow.Launcher.Infrastructure.Storage
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 @@ namespace Flow.Launcher.Infrastructure.Storage
File.Replace(TempFilePath, FilePath, BackupFilePath);
}
}
+
}
}
diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs
index d5eea4fa5..619a94deb 100644
--- a/Flow.Launcher.Plugin/AllowedLanguage.cs
+++ b/Flow.Launcher.Plugin/AllowedLanguage.cs
@@ -12,6 +12,11 @@ namespace Flow.Launcher.Plugin
///
public const string Python = "Python";
+ ///
+ /// Python V2
+ ///
+ public const string PythonV2 = "Python_v2";
+
///
/// C#
///
@@ -27,16 +32,31 @@ namespace Flow.Launcher.Plugin
///
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 @@ namespace Flow.Launcher.Plugin
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 @@ namespace Flow.Launcher.Plugin
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 672285840..b41675a1a 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 @@ namespace Flow.Launcher.Plugin
///
public string ActionKeyword { get; init; }
+ [JsonIgnore]
///
/// Splits by spaces and returns the first item.
///
@@ -71,12 +75,14 @@ namespace Flow.Launcher.Plugin
/// 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 @@ namespace Flow.Launcher.Plugin
///
/// returns an empty string when does not have enough items.
///
+ [JsonIgnore]
public string SecondSearch => SplitSearch(1);
///
@@ -93,6 +100,7 @@ namespace Flow.Launcher.Plugin
///
/// 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 d071545ba..3d05e5679 100644
--- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
+++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
@@ -53,21 +53,14 @@ namespace Flow.Launcher.Test.Plugins
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 @@ namespace Flow.Launcher.Test.Plugins
}
}
-}
\ No newline at end of file
+}