diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs new file mode 100644 index 00000000000..9ebacc9422b --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -0,0 +1,234 @@ +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + public abstract class AbstractPluginEnvironment + { + internal abstract string Language { get; } + + internal abstract string EnvName { get; } + + internal abstract string EnvPath { get; } + + internal abstract string InstallPath { get; } + + internal abstract string ExecutablePath { get; } + + internal virtual string FileDialogFilter => string.Empty; + + internal abstract string PluginsSettingsFilePath { get; set; } + + internal List PluginMetadataList; + + internal PluginsSettings PluginSettings; + + internal AbstractPluginEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) + { + PluginMetadataList = pluginMetadataList; + PluginSettings = pluginSettings; + } + + internal IEnumerable Setup() + { + if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) + return new List(); + + // TODO: Remove. This is backwards compatibility for 1.10.0 release- changed PythonEmbeded to Environments/Python + if (Language.Equals(AllowedLanguage.Python, StringComparison.OrdinalIgnoreCase)) + { + FilesFolders.RemoveFolderIfExists(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable")); + + if (!string.IsNullOrEmpty(PluginSettings.PythonDirectory) && PluginSettings.PythonDirectory.StartsWith(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"))) + { + InstallEnvironment(); + PluginSettings.PythonDirectory = string.Empty; + } + } + + if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) + { + // Ensure latest only if user is using Flow's environment setup. + if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) + EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath); + + return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + } + + if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " + + $"will require {EnvName} to run. Would you like to download {EnvName}? " + + Environment.NewLine + Environment.NewLine + + "Click no if it's already installed, " + + $"and you will be prompted to select the folder that contains the {EnvName} executable", + string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) + { + var msg = $"Please select the {EnvName} executable"; + var selectedFile = string.Empty; + + selectedFile = GetFileFromDialog(msg, FileDialogFilter); + + if (!string.IsNullOrEmpty(selectedFile)) + PluginsSettingsFilePath = selectedFile; + + // Nothing selected because user pressed cancel from the file dialog window + if (string.IsNullOrEmpty(selectedFile)) + InstallEnvironment(); + } + else + { + InstallEnvironment(); + } + + if (FilesFolders.FileExists(PluginsSettingsFilePath)) + { + return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + } + else + { + MessageBox.Show( + $"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom)."); + Log.Error("PluginsLoader", + $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", + $"{Language}Environment"); + + return new List(); + } + } + + internal abstract void InstallEnvironment(); + + private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath) + { + if (expectedPath == currentPath) + return; + + FilesFolders.RemoveFolderIfExists(installedDirPath); + + InstallEnvironment(); + + } + + internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata); + + private IEnumerable SetPathForPluginPairs(string filePath, string languageToSet) + { + var pluginPairs = new List(); + + foreach (var metadata in PluginMetadataList) + { + if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase)) + pluginPairs.Add(CreatePluginPair(filePath, metadata)); + } + + return pluginPairs; + } + + private string GetFileFromDialog(string title, string filter = "") + { + var dlg = new OpenFileDialog + { + InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + Multiselect = false, + CheckFileExists = true, + CheckPathExists = true, + Title = title, + Filter = filter + }; + + var result = dlg.ShowDialog(); + if (result == DialogResult.OK) + { + return dlg.FileName; + } + else + { + return string.Empty; + } + } + + /// + /// After app updated while in portable mode or switched between portable/roaming mode, + /// need to update each plugin's executable path so user will not be prompted again to reinstall the environments. + /// + /// + public static void PreStartPluginExecutablePathUpdate(Settings settings) + { + if (DataLocation.PortableDataLocationInUse()) + { + // When user is using portable but has moved flow to a different location + if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName) + && !settings.PluginSettings.PythonExecutablePath.StartsWith(DataLocation.PortableDataPath)) + { + settings.PluginSettings.PythonExecutablePath + = GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath); + } + + if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName) + && !settings.PluginSettings.NodeExecutablePath.StartsWith(DataLocation.PortableDataPath)) + { + settings.PluginSettings.NodeExecutablePath + = GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath); + } + + // When user has switched from roaming to portable + if (IsUsingRoamingPath(settings.PluginSettings.PythonExecutablePath)) + { + settings.PluginSettings.PythonExecutablePath + = settings.PluginSettings.PythonExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath); + } + + if (IsUsingRoamingPath(settings.PluginSettings.NodeExecutablePath)) + { + settings.PluginSettings.NodeExecutablePath + = settings.PluginSettings.NodeExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath); + } + } + else + { + if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)) + settings.PluginSettings.PythonExecutablePath + = GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath); + + if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)) + settings.PluginSettings.NodeExecutablePath + = GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath); + } + } + + private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName) + { + if (string.IsNullOrEmpty(filePath)) + return false; + + // DataLocation.PortableDataPath returns the current portable path, this determines if an out + // of date path is also a portable path. + var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}"; + + return filePath.Contains(portableAppEnvLocation); + } + + private static bool IsUsingRoamingPath(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + return false; + + return filePath.StartsWith(DataLocation.RoamingDataPath); + } + + private static string GetUpdatedEnvironmentPath(string filePath) + { + var index = filePath.IndexOf(DataLocation.PluginEnvironments); + + // get the substring after "Environments" because we can not determine it dynamically + var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count()); + return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}"; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs new file mode 100644 index 00000000000..b67059b1b6b --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + + internal class JavaScriptEnvironment : TypeScriptEnvironment + { + internal override string Language => AllowedLanguage.JavaScript; + + internal JavaScriptEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs new file mode 100644 index 00000000000..55b3b60bdcf --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -0,0 +1,48 @@ +using Droplex; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; +using System.Collections.Generic; +using System.IO; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class PythonEnvironment : AbstractPluginEnvironment + { + internal override string Language => AllowedLanguage.Python; + + internal override string EnvName => DataLocation.PythonEnvironmentName; + + internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName); + + internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.8.9"); + + internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe"); + + internal override string FileDialogFilter => "Python|pythonw.exe"; + + internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; } + + internal PythonEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + + internal override void InstallEnvironment() + { + FilesFolders.RemoveFolderIfExists(InstallPath); + + // Python 3.8.9 is used for Windows 7 compatibility + DroplexPackage.Drop(App.python_3_8_9_embeddable, InstallPath).Wait(); + + PluginsSettingsFilePath = ExecutablePath; + } + + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new PythonPlugin(filePath), + Metadata = metadata + }; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs new file mode 100644 index 00000000000..70341f711f1 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Droplex; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin; +using System.IO; +using Flow.Launcher.Core.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class TypeScriptEnvironment : AbstractPluginEnvironment + { + internal override string Language => AllowedLanguage.TypeScript; + + 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 TypeScriptEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + + internal override void InstallEnvironment() + { + FilesFolders.RemoveFolderIfExists(InstallPath); + + DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); + + PluginsSettingsFilePath = ExecutablePath; + } + + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new NodePlugin(filePath), + Metadata = metadata + }; + } + } +} diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index ec99dc520ef..beb2925bff0 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -53,7 +53,7 @@ - + diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 049d1c5833e..6b55bb3e3d2 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -9,7 +9,6 @@ namespace Flow.Launcher.Core.Plugin internal class ExecutablePlugin : JsonRPCPlugin { private readonly ProcessStartInfo _startInfo; - public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; public ExecutablePlugin(string filename) { diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 4df5037a574..28d57501baa 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -37,10 +37,7 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProv { protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; - /// - /// The language this JsonRPCPlugin support - /// - public abstract string SupportedLanguage { get; set; } + protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default); diff --git a/Flow.Launcher.Core/Plugin/NodePlugin.cs b/Flow.Launcher.Core/Plugin/NodePlugin.cs new file mode 100644 index 00000000000..1247143fabe --- /dev/null +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Execution of JavaScript & TypeScript plugins + /// + internal class NodePlugin : JsonRPCPlugin + { + private readonly ProcessStartInfo _startInfo; + + public NodePlugin(string filename) + { + _startInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + } + + protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) + { + _startInfo.ArgumentList[1] = request.ToString(); + 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(); + 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; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 7521742636a..e6329aba170 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -1,30 +1,38 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using System.Windows.Forms; -using Droplex; -using Flow.Launcher.Infrastructure; +using Flow.Launcher.Core.ExternalPlugins.Environments; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedCommands; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Core.Plugin { public static class PluginsLoader { - public const string PythonExecutable = "pythonw.exe"; - public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); - var pythonPlugins = PythonPlugins(metadatas, settings); + + var pythonEnv = new PythonEnvironment(metadatas, settings); + var tsEnv = new TypeScriptEnvironment(metadatas, settings); + var jsEnv = new JavaScriptEnvironment(metadatas, settings); + var pythonPlugins = pythonEnv.Setup(); + var tsPlugins = tsEnv.Setup(); + var jsPlugins = jsEnv.Setup(); + var executablePlugins = ExecutablePlugins(metadatas); - var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList(); + + var plugins = dotnetPlugins + .Concat(pythonPlugins) + .Concat(tsPlugins) + .Concat(jsPlugins) + .Concat(executablePlugins) + .ToList(); return plugins; } @@ -108,97 +116,10 @@ public static IEnumerable DotNetPlugins(List source) return plugins; } - public static IEnumerable PythonPlugins(List source, PluginsSettings settings) - { - if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) - return new List(); - - if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory)) - return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable)); - - var pythonPath = string.Empty; - - if (MessageBox.Show("Flow detected you have installed Python plugins, which " + - "will need Python to run. Would you like to download Python? " + - Environment.NewLine + Environment.NewLine + - "Click no if it's already installed, " + - "and you will be prompted to select the folder that contains the Python executable", - string.Empty, MessageBoxButtons.YesNo) == DialogResult.No - && string.IsNullOrEmpty(settings.PythonDirectory)) - { - var dlg = new FolderBrowserDialog - { - SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) - }; - - var result = dlg.ShowDialog(); - if (result == DialogResult.OK) - { - string pythonDirectory = dlg.SelectedPath; - if (!string.IsNullOrEmpty(pythonDirectory)) - { - pythonPath = Path.Combine(pythonDirectory, PythonExecutable); - if (File.Exists(pythonPath)) - { - settings.PythonDirectory = pythonDirectory; - Constant.PythonPath = pythonPath; - } - else - { - MessageBox.Show("Can't find python in given directory"); - } - } - } - } - else - { - var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"); - - // Python 3.8.9 is used for Windows 7 compatibility - DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait(); - - pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable); - if (FilesFolders.FileExists(pythonPath)) - { - settings.PythonDirectory = installedPythonDirectory; - Constant.PythonPath = pythonPath; - } - else - { - Log.Error("PluginsLoader", - $"Failed to set Python path after Droplex install, {pythonPath} does not exist", - "PythonPlugins"); - } - } - - if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath)) - { - MessageBox.Show( - "Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom)."); - Log.Error("PluginsLoader", - $"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.", - "PythonPlugins"); - - return new List(); - } - - return SetPythonPathForPluginPairs(source, pythonPath); - } - - private static IEnumerable SetPythonPathForPluginPairs(List source, string pythonPath) - => source - .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) - .Select(metadata => new PluginPair - { - Plugin = new PythonPlugin(pythonPath), - Metadata = metadata - }) - .ToList(); - public static IEnumerable ExecutablePlugins(IEnumerable source) { return source - .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) + .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) .Select(metadata => new PluginPair { Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 8f7e5760af4..2bbf6d11088 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading; @@ -11,7 +11,6 @@ namespace Flow.Launcher.Core.Plugin internal class PythonPlugin : JsonRPCPlugin { private readonly ProcessStartInfo _startInfo; - public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; public PythonPlugin(string filename) { @@ -46,6 +45,7 @@ protected override Task RequestAsync(JsonRPCRequestModel request, Cancel protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { + // since this is not static, request strings will build up in ArgumentList if index is not specified _startInfo.ArgumentList[2] = rpcRequest.ToString(); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 44c47cf289d..a0baa51ba5e 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -144,6 +144,5 @@ public string NewVersionTips(string version) return tips; } - } } diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 56f421e30cc..ab5e4722bf0 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -32,6 +32,7 @@ public static class Constant public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); public static string PythonPath; + public static string NodePath; public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.svg"; diff --git a/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs b/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs index 40ac6b1216d..54c19c0482d 100644 --- a/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs +++ b/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs @@ -67,6 +67,7 @@ private static string CreateExceptionReport(System.Exception ex) sb.AppendLine($"* IntPtr Length: {IntPtr.Size}"); sb.AppendLine($"* x64: {Environment.Is64BitOperatingSystem}"); sb.AppendLine($"* Python Path: {Constant.PythonPath}"); + sb.AppendLine($"* Node Path: {Constant.NodePath}"); sb.AppendLine($"* CLR Version: {Environment.Version}"); sb.AppendLine($"* Installed .NET Framework: "); foreach (var result in GetFrameworkVersionFromRegistry()) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index e93c341dcd7..e294f52b8c5 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; +using System; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Flow.Launcher.Infrastructure.UserSettings { @@ -31,5 +27,10 @@ public static bool PortableDataLocationInUse() public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); + + public const string PythonEnvironmentName = "Python"; + public const string NodeEnvironmentName = "Node.js"; + public const string PluginEnvironments = "Environments"; + public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments); } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index c06e1587dca..130e25d7bf1 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -1,11 +1,34 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { public class PluginsSettings : BaseModel { + private string pythonExecutablePath = string.Empty; + public string PythonExecutablePath { + get { return pythonExecutablePath; } + set + { + pythonExecutablePath = value; + Constant.PythonPath = value; + } + } + + private string nodeExecutablePath = string.Empty; + public string NodeExecutablePath + { + get { return nodeExecutablePath; } + set + { + nodeExecutablePath = value; + Constant.NodePath = value; + } + } + + // TODO: Remove. This is backwards compatibility for 1.10.0 release. public string PythonDirectory { get; set; } + public Dictionary Plugins { get; set; } = new Dictionary(); public void UpdatePluginSettings(List metadatas) diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index 94c645d2761..d5eea4fa56b 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -1,4 +1,6 @@ -namespace Flow.Launcher.Plugin +using System; + +namespace Flow.Launcher.Plugin { /// /// Allowed plugin languages @@ -8,34 +10,32 @@ public static class AllowedLanguage /// /// Python /// - public static string Python - { - get { return "PYTHON"; } - } + public const string Python = "Python"; /// /// C# /// - public static string CSharp - { - get { return "CSHARP"; } - } + public const string CSharp = "CSharp"; /// /// F# /// - public static string FSharp - { - get { return "FSHARP"; } - } + public const string FSharp = "FSharp"; /// /// Standard .exe /// - public static string Executable - { - get { return "EXECUTABLE"; } - } + public const string Executable = "Executable"; + + /// + /// TypeScript + /// + public const string TypeScript = "TypeScript"; + + /// + /// JavaScript + /// + public const string JavaScript = "JavaScript"; /// /// Determines if this language is a .NET language @@ -44,8 +44,8 @@ public static string Executable /// public static bool IsDotNet(string language) { - return language.ToUpper() == CSharp - || language.ToUpper() == FSharp; + return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) + || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); } /// @@ -56,8 +56,10 @@ public static bool IsDotNet(string language) public static bool IsAllowed(string language) { return IsDotNet(language) - || language.ToUpper() == Python.ToUpper() - || language.ToUpper() == Executable.ToUpper(); + || language.Equals(Python, StringComparison.OrdinalIgnoreCase) + || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index fb91c6388bd..765280e08f7 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -1,4 +1,4 @@ -using NUnit; +using NUnit; using NUnit.Framework; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; @@ -16,8 +16,6 @@ namespace Flow.Launcher.Test.Plugins // ReSharper disable once InconsistentNaming internal class JsonRPCPluginTest : JsonRPCPlugin { - public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; - protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { throw new System.NotImplementedException(); diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 35037146123..43fa0eddb1f 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -6,6 +6,7 @@ using System.Windows; using Flow.Launcher.Core; using Flow.Launcher.Core.Configuration; +using Flow.Launcher.Core.ExternalPlugins.Environments; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -61,6 +62,8 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; + AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); + _alphabet.Initialize(_settings); _stringMatcher = new StringMatcher(_alphabet); StringMatcher.Instance = _stringMatcher; diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index f3f590167b1..a7ce7444cc6 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -43,8 +43,8 @@ public static string RuntimeInfo() public static string DependenciesInfo() { - var info = $"\nPython Path: {Constant.PythonPath}"; + var info = $"\nPython Path: {Constant.PythonPath}\nNode Path: {Constant.NodePath}"; return info; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Languages/da.xaml b/Flow.Launcher/Languages/da.xaml index e6eec4560b2..cc3d9d52735 100644 --- a/Flow.Launcher/Languages/da.xaml +++ b/Flow.Launcher/Languages/da.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Python bibliotek Autoopdatering - Vælg + Vælg Skjul Flow Launcher ved opstart Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml index 2073fd42470..dc7d669f444 100644 --- a/Flow.Launcher/Languages/de.xaml +++ b/Flow.Launcher/Languages/de.xaml @@ -53,9 +53,8 @@ Wählen Sie den Dateimanager, der beim Öffnen des Ordners verwendet werden soll. Standardbrowser Einstellung für neuen Tab, neues Fenster und dem Privatmodus. - Python-Verzeichnis Automatische Aktualisierung - Auswählen + Auswählen Verstecke Flow Launcher bei Systemstart Statusleistensymbol ausblenden When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 550b73f85db..5c7caafd731 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -57,11 +57,14 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. + Python Path + Node.js Path + Please select the Node.js executable + Please select pythonw.exe Always Start Typing in English Mode Temporarily change your input method to English mode when activating Flow. - Python Directory Auto Update - Select + Select Hide Flow Launcher on startup Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. @@ -293,7 +296,7 @@ Please wait... - + Checking for new update You already have the latest Flow Launcher version Update found diff --git a/Flow.Launcher/Languages/es-419.xaml b/Flow.Launcher/Languages/es-419.xaml index 3a4317ca9cb..3ac0e22d841 100644 --- a/Flow.Launcher/Languages/es-419.xaml +++ b/Flow.Launcher/Languages/es-419.xaml @@ -53,9 +53,8 @@ Seleccione el gestor de archivos a utilizar al abrir la carpeta. Navegador web predeterminado Configuración para Nueva Pestaña, Nueva Ventana, Modo Privado. - Directorio de Python Actualización automática - Seleccionar + Seleccionar Ocultar Flow Launcher al arrancar el sistema Ocultar icono de la bandeja When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/es.xaml b/Flow.Launcher/Languages/es.xaml index b3dbaf4fff6..03fdfb21ca7 100644 --- a/Flow.Launcher/Languages/es.xaml +++ b/Flow.Launcher/Languages/es.xaml @@ -53,9 +53,8 @@ Selecciona el administrador de archivos que se desea utilizar para abrir la carpeta. Navegador web predeterminado Configuración para Nueva Pestaña, Nueva Ventana, Modo Privado. - Carpeta de Python Actualización automática - Seleccionar + Seleccionar Ocultar Flow Launcher al inicio Ocultar icono de la bandeja del sistema Cuando el icono está oculto en la bandeja del sistema, se puede abrir el menú de configuración haciendo clic con el botón derecho en la ventana de búsqueda. diff --git a/Flow.Launcher/Languages/fr.xaml b/Flow.Launcher/Languages/fr.xaml index 4664b871dc8..7ebd158c230 100644 --- a/Flow.Launcher/Languages/fr.xaml +++ b/Flow.Launcher/Languages/fr.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Navigateur web par défaut Setting for New Tab, New Window, Private Mode. - Répertoire de Python Mettre à jour automatiquement - Sélectionner + Sélectionner Cacher Flow Launcher au démarrage Masquer icône du plateau When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/it.xaml b/Flow.Launcher/Languages/it.xaml index 84fcad8c89c..1aabba84d76 100644 --- a/Flow.Launcher/Languages/it.xaml +++ b/Flow.Launcher/Languages/it.xaml @@ -53,9 +53,8 @@ Selezionare il Gestore file da usare all'apertura della cartella. Browser predefinito Impostazione per Nuova scheda, Nuova finestra, Modalità privata. - Cartella Python Aggiornamento automatico - Seleziona + Seleziona Nascondi Flow Launcher all'avvio Nascondi Icona nell'Area di Notifica When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/ja.xaml b/Flow.Launcher/Languages/ja.xaml index 816602f1c53..d9302130194 100644 --- a/Flow.Launcher/Languages/ja.xaml +++ b/Flow.Launcher/Languages/ja.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Pythonのディレクトリ 自動更新 - 選択 + 選択 起動時にFlow Launcherを隠す トレイアイコンを隠す When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index f89a2845481..3c79cc045fd 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -53,9 +53,8 @@ 폴더를 열 때 사용할 파일관리자를 선택하세요. 기본 웹 브라우저 새 탭, 새 창, 사생활 보호 모드 - Python 디렉토리 자동 업데이트 - 선택 + 선택 시작 시 Flow Launcher 숨김 트레이 아이콘 숨기기 트레이에서 아이콘을 숨길 경우, 검색창 우클릭으로 설정창을 열 수 있습니다. diff --git a/Flow.Launcher/Languages/nb-NO.xaml b/Flow.Launcher/Languages/nb-NO.xaml index 859ec20a0bb..c8b1e4456b6 100644 --- a/Flow.Launcher/Languages/nb-NO.xaml +++ b/Flow.Launcher/Languages/nb-NO.xaml @@ -28,9 +28,8 @@ Tøm siste spørring Maks antall resultater vist Ignorer hurtigtaster i fullskjermsmodus - Python-mappe Oppdater automatisk - Velg + Velg Skjul Flow Launcher ved oppstart diff --git a/Flow.Launcher/Languages/nb.xaml b/Flow.Launcher/Languages/nb.xaml index 5356890e314..017a2064a8d 100644 --- a/Flow.Launcher/Languages/nb.xaml +++ b/Flow.Launcher/Languages/nb.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Python Directory Auto Update - Select + Select Hide Flow Launcher on startup Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/nl.xaml b/Flow.Launcher/Languages/nl.xaml index a7af3cc57af..19ea7cda66d 100644 --- a/Flow.Launcher/Languages/nl.xaml +++ b/Flow.Launcher/Languages/nl.xaml @@ -53,9 +53,8 @@ Selecteer de bestandsbeheerder voor het openen van de map. Standaard webbrowser Instelling voor Nieuw tabblad, Nieuw Venster, Privémodus. - Python map Automatische Update - Selecteer + Selecteer Verberg Flow Launcher als systeem opstart Systeemvakpictogram verbergen When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/pl.xaml b/Flow.Launcher/Languages/pl.xaml index 0a7ccf07061..17ad0fec760 100644 --- a/Flow.Launcher/Languages/pl.xaml +++ b/Flow.Launcher/Languages/pl.xaml @@ -53,9 +53,8 @@ Wybierz menedżer plików używany do otwierania folderów. Domyślna przeglądarka Ustawienie dla nowej karty, nowego okna i trybu prywatnego. - Folder biblioteki Python Automatyczne aktualizacje - Wybierz + Wybierz Uruchamiaj Flow Launcher zminimalizowany Ukryj ikonę zasobnika When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/pt-br.xaml b/Flow.Launcher/Languages/pt-br.xaml index c308d336747..23dfbc56159 100644 --- a/Flow.Launcher/Languages/pt-br.xaml +++ b/Flow.Launcher/Languages/pt-br.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Diretório Python Atualizar Automaticamente - Selecionar + Selecionar Esconder Flow Launcher na inicialização Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/pt-pt.xaml b/Flow.Launcher/Languages/pt-pt.xaml index a06d0e935d6..1590d0896f3 100644 --- a/Flow.Launcher/Languages/pt-pt.xaml +++ b/Flow.Launcher/Languages/pt-pt.xaml @@ -53,9 +53,8 @@ Selecione o gestor de ficheiros utilizado para abrir a página Navegador web padrão Definições para Novo separador, Nova Janela e Modo privado - Diretório Python Atualização automática - Selecionar + Selecionar Ocultar Flow Launcher ao arrancar Ocultar ícone na bandeja Se o ícone da bandeja estiver oculto, pode abrir as Definições com um clique com o botão direito do rato na caixa de pesquisa. diff --git a/Flow.Launcher/Languages/ru.xaml b/Flow.Launcher/Languages/ru.xaml index 471f663eb59..d6582c58877 100644 --- a/Flow.Launcher/Languages/ru.xaml +++ b/Flow.Launcher/Languages/ru.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Python Directory Auto Update - Select + Select Hide Flow Launcher on startup Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 97223e46c03..00ce6b7ca7b 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -53,9 +53,8 @@ Vyberte správcu súborov, ktorý sa má použiť pri otváraní priečinka. Predvolený webový prehliadač Nastavenie pre novú kartu, nové okno, privátny režim. - Priečinok s Pythonom Automatická aktualizácia - Vybrať + Vybrať Schovať Flow Launcher po spustení Schovať ikonu z oblasti oznámení Keď je ikona skrytá z oblasti oznámení, nastavenia možno otvoriť kliknutím pravým tlačidlom myši na okno vyhľadávania. diff --git a/Flow.Launcher/Languages/sr.xaml b/Flow.Launcher/Languages/sr.xaml index 0ffb86fac60..f32b4465221 100644 --- a/Flow.Launcher/Languages/sr.xaml +++ b/Flow.Launcher/Languages/sr.xaml @@ -53,9 +53,8 @@ Select the file manager to use when opening the folder. Default Web Browser Setting for New Tab, New Window, Private Mode. - Python direktorijum Auto ažuriranje - Izaberi + Izaberi Sakrij Flow Launcher pri podizanju sistema Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/tr.xaml b/Flow.Launcher/Languages/tr.xaml index c9c11dbd5f2..d47f0a7401d 100644 --- a/Flow.Launcher/Languages/tr.xaml +++ b/Flow.Launcher/Languages/tr.xaml @@ -53,9 +53,8 @@ Klasör açarken kullanılacak dosya yöneticisini seçin. Varsayılan Tarayıcısı Yeni Sekme, Yeni Pencere, Gizli Mod için Ayar. - Python Konumu Otomatik Güncelle - Seç + Seç Başlangıçta Flow Launcher'u gizle Sistem çekmecesi simgesini gizle When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/uk-UA.xaml b/Flow.Launcher/Languages/uk-UA.xaml index e8b2048d83f..c31bebe2518 100644 --- a/Flow.Launcher/Languages/uk-UA.xaml +++ b/Flow.Launcher/Languages/uk-UA.xaml @@ -53,9 +53,8 @@ Виберіть файловий менеджер для використання під час відкриття теки. Браузер за замовчуванням Налаштування нової вкладки, нового вікна, приватного режиму. - Директорія Python Автоматичне оновлення - Вибрати + Вибрати Сховати Flow Launcher при запуску системи Приховати значок в системному лотку When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index e4518d6c388..d969489516b 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -53,9 +53,8 @@ 选择打开文件夹时要使用的文件管理器。 默认浏览器 新标签/窗口及隐身模式设置。 - Python 路径 自动更新 - 选择 + 选择 系统启动时不显示主窗口 隐藏任务栏图标 任务栏图标被隐藏时,右键点击搜索窗口即可打开设置菜单。 diff --git a/Flow.Launcher/Languages/zh-tw.xaml b/Flow.Launcher/Languages/zh-tw.xaml index e5a8800f783..3757c7d4d67 100644 --- a/Flow.Launcher/Languages/zh-tw.xaml +++ b/Flow.Launcher/Languages/zh-tw.xaml @@ -53,9 +53,8 @@ 選擇開啟資料夾時要使用的檔案管理器。 預設瀏覽器 設定新增分頁、視窗和無痕模式。 - Python 路徑 自動更新 - 選擇 + 選擇 啟動時不顯示主視窗 隱藏任務欄圖標 When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. diff --git a/Flow.Launcher/SelectBrowserWindow.xaml b/Flow.Launcher/SelectBrowserWindow.xaml index 88fd581b450..d8807dbef90 100644 --- a/Flow.Launcher/SelectBrowserWindow.xaml +++ b/Flow.Launcher/SelectBrowserWindow.xaml @@ -166,7 +166,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Click="btnBrowseFile_Click" - Content="{DynamicResource selectPythonDirectory}" + Content="{DynamicResource select}" DockPanel.Dock="Right">