Merge branch 'dev' into shell_fix

This commit is contained in:
flox_x 2023-11-09 14:24:49 +01:00 committed by GitHub
commit 80bed38fae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1400 additions and 563 deletions

View file

@ -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>

View file

@ -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) { }
}
}

View file

@ -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) { }
}
}

View file

@ -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
};
}
}
}

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Logger;
using System;
using System.Collections.Generic;
using System.Threading;

View file

@ -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>

View file

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

View 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
};
}
}
}

View file

@ -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

View file

@ -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;
}
}
}
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -0,0 +1,4 @@
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{
public record JsonRPCExecuteResponse(bool Hide = true);
}

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

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{
public record JsonRPCQueryRequest(
List<JsonRPCResult> Results
);
}

View file

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

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

View file

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

View 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();
}
}
}

View file

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

View 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");
}
}
}

View file

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

View file

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

View file

@ -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)

View file

@ -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
}
}
}
}