diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 049d1c5833e..dbf6773b7b7 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -21,12 +21,15 @@ public ExecutablePlugin(string filename) RedirectStandardOutput = true, RedirectStandardError = true }; - - // required initialisation for below request calls + + // required initialisation for below request calls _startInfo.ArgumentList.Add(string.Empty); } - protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) + protected override Task RequestAsync( + JsonRPCRequestModel request, + CancellationToken token = default, + bool ignoreEmptyResponse = false) { // since this is not static, request strings will build up in ArgumentList if index is not specified _startInfo.ArgumentList[0] = request.ToString(); diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 4cfa83382ba..102c44e0200 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -35,7 +35,7 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable, IPathSelected { protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; @@ -44,7 +44,11 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProv /// The language this JsonRPCPlugin support /// public abstract string SupportedLanguage { get; set; } - protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); + protected abstract Task RequestAsync( + JsonRPCRequestModel rpcRequest, + CancellationToken token = default, + bool ignoreEmptyResponse = false); + protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default); private static readonly RecyclableMemoryStreamManager BufferManager = new(); @@ -238,7 +242,10 @@ protected string Execute(ProcessStartInfo startInfo) } } - protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) + protected async Task ExecuteAsync( + ProcessStartInfo startInfo, + CancellationToken token = default, + bool ignoreEmptyResponse = false) { Process process = null; bool disposed = false; @@ -265,7 +272,7 @@ protected async Task ExecuteAsync(ProcessStartInfo startInfo, Cancellati try { - // token expire won't instantly trigger the exception, + // token expire won't instantly trigger the exception, // manually kill process at before await source.CopyToAsync(buffer, token); } @@ -281,10 +288,13 @@ protected async Task ExecuteAsync(ProcessStartInfo startInfo, Cancellati if (buffer.Length == 0) { - var errorMessage = process.StandardError.EndOfStream ? + var endOfStream = process.StandardError.EndOfStream; + var errorMessage = endOfStream ? "Empty JSONRPC Response" : await process.StandardError.ReadToEndAsync(); - throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}"); + + if (!endOfStream || (endOfStream && !ignoreEmptyResponse)) + throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}"); } if (!process.StandardError.EndOfStream) @@ -534,5 +544,18 @@ public void UpdateSettings(Dictionary settings) } } } + + public async Task PathSelected(string filePath, string hotkey) + { + var request = new JsonRPCRequestModel + { + Method = "PathSelected", + Parameters = new object[] + { + filePath, hotkey + } + }; + await RequestAsync(request, ignoreEmptyResponse: true); + } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 134c3c002dc..094570f0801 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -26,7 +26,6 @@ public static class PluginManager public static IPublicAPI API { private set; get; } - // todo happlebao, this should not be public, the indicator function should be embeded public static PluginsSettings Settings; private static List _metadatas; @@ -179,6 +178,20 @@ public static ICollection ValidPluginsForQuery(Query query) } } + public static async Task PublishPathSelectedFromResult(string selectedPath, string hotkey) + { + await Task.WhenAll(AllPlugins.Select(pluginPair => pluginPair.Plugin switch + { + IPathSelected p + when pluginPair + .Metadata + .Listeners + .Any(x => x.ContainsKey(hotkey) && x.ContainsValue("IPathSelected")) + => p.PathSelected(selectedPath, hotkey), + _ => Task.CompletedTask, + })); + } + public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 8f7e5760af4..60d34b27984 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -37,11 +37,14 @@ public PythonPlugin(string filename) _startInfo.ArgumentList.Add("-B"); } - protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) + protected override Task RequestAsync( + JsonRPCRequestModel request, + CancellationToken token = default, + bool ignoreEmptyResponse = false) { _startInfo.ArgumentList[2] = request.ToString(); - return ExecuteAsync(_startInfo, token); + return ExecuteAsync(_startInfo, token, ignoreEmptyResponse: ignoreEmptyResponse); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) diff --git a/Flow.Launcher.Plugin/Interfaces/IPathSelected.cs b/Flow.Launcher.Plugin/Interfaces/IPathSelected.cs new file mode 100644 index 00000000000..bb7ad6ddc6f --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IPathSelected.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + public interface IPathSelected : IFeatures + { + Task PathSelected(string path, string hotkey); + } +} diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index e8f5cf74432..57bda809ea2 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -35,8 +35,10 @@ internal set public List ActionKeywords { get; set; } + public List> Listeners { get; set;} + public string IcoPath { get; set;} - + public override string ToString() { return Name; diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 714fcc53fa1..ec2d28ebf61 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -39,7 +39,7 @@ - + { SelectedResults.SelectPrevPage(); }); + F1SelectedCommand = new RelayCommand(_ => + { + var results = SelectedResults; + var result = results.SelectedItem?.Result; + + if (result is null) + return; + + var selectedPath = string.Empty; + + if (File.Exists(result.Title) || Directory.Exists(result.Title)) + { + selectedPath = result.Title; + } + else if (File.Exists(result.SubTitle) || Directory.Exists(result.SubTitle)) + { + selectedPath = result.SubTitle; + } + else if (!string.IsNullOrEmpty(result.IcoPath)) + { + selectedPath = result.IcoPath; + } + + PluginManager.PublishPathSelectedFromResult(selectedPath, "f1"); + + }); + SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult()); StartHelpCommand = new RelayCommand(_ => @@ -430,6 +457,8 @@ private ResultsViewModel SelectedResults public ICommand AutocompleteQueryCommand { get; set; } + public ICommand F1SelectedCommand { get; set; } + public string OpenResultCommandModifiers { get; private set; } public string Image => Constant.QueryTextBoxIconImagePath; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 60208759ed0..0114063c1e0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -11,7 +11,7 @@ namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IPathSelected { internal PluginInitContext Context { get; set; } @@ -28,6 +28,14 @@ public Control CreateSettingPanel() return new ExplorerSettings(viewModel); } + public Task PathSelected(string path, string hotkey) + { + //checked the hotkey if is ctrl+c, then + //copy file to clipboard. + Context.API.ShowMsg("Success", string.Format("You have selected path \"{0}\", with hotkey \"{1}\"", path, hotkey)); + return Task.CompletedTask; + } + public Task InitAsync(PluginInitContext context) { Context = context; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index 10be3381d0d..76b124df3ba 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,6 +7,7 @@ "*", "*" ], + "Listeners": [{"f1": "IPathSelected"}], "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu",