mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into shell_fix
This commit is contained in:
commit
80bed38fae
26 changed files with 1400 additions and 563 deletions
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PluginMetadata> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<PackageReference Include="FSharp.Core" Version="7.0.400" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.16.36" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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<Stream> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
26
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonRPCResult> Result,
|
||||
IReadOnlyDictionary<string, object> 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<string, object> 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<JsonRPCResult> Result { get; set; }
|
||||
|
||||
public Dictionary<string, object> SettingsChange { get; set; }
|
||||
|
||||
public string DebugMessage { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCRequestModel
|
||||
{
|
||||
public string Method { get; set; }
|
||||
|
||||
public object[] Parameters { get; set; }
|
||||
|
||||
public Dictionary<string, object> Settings { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request that Flow Launcher sent to client
|
||||
/// </summary>
|
||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request(in query response) that client sent to Flow Launcher
|
||||
/// </summary>
|
||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public bool DontHideAfterAction { get; set; }
|
||||
}
|
||||
|
||||
public record JsonRPCClientRequestModel(
|
||||
int Id,
|
||||
string Method,
|
||||
object[] Parameters,
|
||||
IReadOnlyDictionary<string, object> Settings,
|
||||
bool DontHideAfterAction = false,
|
||||
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||
{
|
||||
protected PluginInitContext context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
protected abstract Task<Stream> 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<Result> 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<Result> 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<string, object> Settings { get; set; }
|
||||
|
||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
|
||||
|
|
@ -103,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return ParseResults(queryResponseModel);
|
||||
}
|
||||
|
||||
|
||||
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||
protected override async Task<bool> 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<JsonRPCRequestModel>(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<JsonRPCRequestModel>(actionResponse, options);
|
||||
|
||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||
{
|
||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||
jsonRpcRequestModel.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute external program and return the output
|
||||
|
|
@ -296,379 +248,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return sourceBuffer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
public override async Task<List<Result>> 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<Dictionary<string, object>>(fileStream, options);
|
||||
}
|
||||
|
||||
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
|
||||
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
|
||||
|
||||
Settings ??= new Dictionary<string, object>();
|
||||
|
||||
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<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
162
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
162
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
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<Result> 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<bool> ExecuteResultAsync(JsonRPCResult result);
|
||||
|
||||
protected JsonRPCPluginSettings Settings { get; set; }
|
||||
|
||||
protected List<Result> 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<Result>();
|
||||
|
||||
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<List<Result>> 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<JsonRpcConfigurationModel>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
|
|
@ -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<string, FrameworkElement> SettingControls { get; } = new();
|
||||
|
||||
public IReadOnlyDictionary<string, object> Inner => Settings;
|
||||
protected Dictionary<string, object> Settings { get; set; }
|
||||
public required IPublicAPI API { get; init; }
|
||||
|
||||
private JsonStorage<Dictionary<string, object>> _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<Dictionary<string, object>>(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<string, object> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
125
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
125
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
|
|
@ -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<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||
argument: result.JsonRPCAction.Parameters);
|
||||
|
||||
return res.Hide;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||
new[] { query },
|
||||
token);
|
||||
|
||||
var results = ParseResults(res);
|
||||
|
||||
return results;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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<string, JsonRPCQueryResponseModel>((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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||
{
|
||||
public record JsonRPCExecuteResponse(bool Hide = true);
|
||||
}
|
||||
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
|
|
@ -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<PluginPair> GetAllPlugins()
|
||||
{
|
||||
return _api.GetAllPlugins();
|
||||
}
|
||||
|
||||
|
||||
public MatchResult FuzzySearch(string query, string stringToCompare)
|
||||
{
|
||||
return _api.FuzzySearch(query, stringToCompare);
|
||||
}
|
||||
|
||||
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
return _api.HttpGetStringAsync(url, token);
|
||||
}
|
||||
|
||||
public Task<Stream> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||
{
|
||||
public record JsonRPCQueryRequest(
|
||||
List<JsonRPCResult> Results
|
||||
);
|
||||
}
|
||||
|
|
@ -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<Stream> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
38
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Execution of JavaScript & TypeScript plugins
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,22 +19,33 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public static List<PluginPair> Plugins(List<PluginMetadata> 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<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> 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<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
68
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Stream> 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);
|
||||
}
|
||||
|
|
|
|||
43
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
43
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T> 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<T>(serialized) ?? await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
|
||||
return Data.NonNull();
|
||||
}
|
||||
|
||||
private async ValueTask<T> LoadBackupOrDefaultAsync()
|
||||
{
|
||||
var backup = await TryLoadBackupAsync();
|
||||
|
||||
return backup ?? LoadDefault();
|
||||
}
|
||||
|
||||
private async ValueTask<T?> TryLoadBackupAsync()
|
||||
{
|
||||
if (!File.Exists(BackupFilePath))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
await using var source = File.OpenRead(BackupFilePath);
|
||||
var data = await JsonSerializer.DeserializeAsync<T>(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<T>(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public const string Python = "Python";
|
||||
|
||||
/// <summary>
|
||||
/// Python V2
|
||||
/// </summary>
|
||||
public const string PythonV2 = "Python_v2";
|
||||
|
||||
/// <summary>
|
||||
/// C#
|
||||
/// </summary>
|
||||
|
|
@ -27,16 +32,31 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public const string Executable = "Executable";
|
||||
|
||||
/// <summary>
|
||||
/// Standard .exe
|
||||
/// </summary>
|
||||
public const string ExecutableV2 = "Executable_V2";
|
||||
|
||||
/// <summary>
|
||||
/// TypeScript
|
||||
/// </summary>
|
||||
public const string TypeScript = "TypeScript";
|
||||
|
||||
/// <summary>
|
||||
/// TypeScript
|
||||
/// </summary>
|
||||
public const string TypeScriptV2 = "TypeScript_V2";
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript
|
||||
/// </summary>
|
||||
public const string JavaScript = "JavaScript";
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript
|
||||
/// </summary>
|
||||
public const string JavaScriptV2 = "JavaScript_V2";
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a .NET language
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
public string ActionKeyword { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||
/// </summary>
|
||||
|
|
@ -71,12 +75,14 @@ namespace Flow.Launcher.Plugin
|
|||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
public string FirstSearch => SplitSearch(0);
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
private string _secondToEndSearch;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// strings from second search (including) to last search
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : "";
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -85,6 +91,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string SecondSearch => SplitSearch(1);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -93,6 +100,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string ThirdSearch => SplitSearch(2);
|
||||
|
||||
private string SplitSearch(int index)
|
||||
|
|
|
|||
|
|
@ -53,21 +53,14 @@ namespace Flow.Launcher.Test.Plugins
|
|||
|
||||
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
|
||||
{
|
||||
new()
|
||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
||||
{
|
||||
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
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue