mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 11:16:51 +02:00
Add 'src/modules/launcher/' from commit '28acd466b352a807910cebbc74477d3173b31511'
git-subtree-dir: src/modules/launcher git-subtree-mainline:852689b3dfgit-subtree-split:28acd466b3
This commit is contained in:
4
src/modules/launcher/Wox.Core/FodyWeavers.xml
Normal file
4
src/modules/launcher/Wox.Core/FodyWeavers.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers>
|
||||
<PropertyChanged />
|
||||
</Weavers>
|
||||
54
src/modules/launcher/Wox.Core/Plugin/ExecutablePlugin.cs
Normal file
54
src/modules/launcher/Wox.Core/Plugin/ExecutablePlugin.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
internal class ExecutablePlugin : JsonRPCPlugin
|
||||
{
|
||||
private readonly ProcessStartInfo _startInfo;
|
||||
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
|
||||
|
||||
public ExecutablePlugin(string filename)
|
||||
{
|
||||
_startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = filename,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
}
|
||||
|
||||
protected override string ExecuteQuery(Query query)
|
||||
{
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||
{
|
||||
Method = "query",
|
||||
Parameters = new object[] { query.Search },
|
||||
};
|
||||
|
||||
_startInfo.Arguments = $"\"{request}\"";
|
||||
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||
{
|
||||
_startInfo.Arguments = $"\"{rpcRequest}\"";
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
protected override string ExecuteContextMenu(Result selectedResult) {
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
|
||||
Method = "contextmenu",
|
||||
Parameters = new object[] { selectedResult.ContextData },
|
||||
};
|
||||
|
||||
_startInfo.Arguments = $"\"{request}\"";
|
||||
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/modules/launcher/Wox.Core/Plugin/JsonPRCModel.cs
Normal file
135
src/modules/launcher/Wox.Core/Plugin/JsonPRCModel.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Wox and other plugins,
|
||||
* like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the
|
||||
* "id" and "jsonrpc" in the request, since it's not so useful in our request model.
|
||||
*
|
||||
* When execute a query:
|
||||
* Wox -------JsonRPCServerRequestModel--------> client
|
||||
* Wox <------JsonRPCQueryResponseModel--------- client
|
||||
*
|
||||
* When execute a action (which mean user select an item in reulst item):
|
||||
* Wox -------JsonRPCServerRequestModel--------> client
|
||||
* Wox <------JsonRPCResponseModel-------------- client
|
||||
*
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
public class JsonRPCErrorModel
|
||||
{
|
||||
public int Code { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCModelBase
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCResponseModel : JsonRPCModelBase
|
||||
{
|
||||
public string Result { get; set; }
|
||||
|
||||
public JsonRPCErrorModel Error { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCQueryResponseModel : JsonRPCResponseModel
|
||||
{
|
||||
public new List<JsonRPCResult> Result { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCRequestModel : JsonRPCModelBase
|
||||
{
|
||||
public string Method { get; set; }
|
||||
|
||||
public object[] Parameters { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = string.Empty;
|
||||
if (Parameters != null && Parameters.Length > 0)
|
||||
{
|
||||
string parameters = Parameters.Aggregate("[", (current, o) => current + (GetParameterByType(o) + ","));
|
||||
parameters = parameters.Substring(0, parameters.Length - 1) + "]";
|
||||
rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":{1}", Method, parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":[]", Method);
|
||||
}
|
||||
|
||||
return rpc;
|
||||
|
||||
}
|
||||
|
||||
private string GetParameterByType(object parameter)
|
||||
{
|
||||
if (parameter == null) {
|
||||
return "null";
|
||||
}
|
||||
if (parameter is string)
|
||||
{
|
||||
return string.Format(@"\""{0}\""", ReplaceEscapes(parameter.ToString()));
|
||||
}
|
||||
if (parameter is int || parameter is float || parameter is double)
|
||||
{
|
||||
return string.Format(@"{0}", parameter);
|
||||
}
|
||||
if (parameter is bool)
|
||||
{
|
||||
return string.Format(@"{0}", parameter.ToString().ToLower());
|
||||
}
|
||||
return parameter.ToString();
|
||||
}
|
||||
|
||||
private string ReplaceEscapes(string str)
|
||||
{
|
||||
return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo
|
||||
.Replace(@"\", @"\\") //Escapes itself when passed to client
|
||||
.Replace(@"""", @"\\""""");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request that Wox sent to client
|
||||
/// </summary>
|
||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = base.ToString();
|
||||
return rpc + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request(in query response) that client sent to Wox
|
||||
/// </summary>
|
||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public bool DontHideAfterAction { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string rpc = base.ToString();
|
||||
return rpc + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represent the json-rpc result item that client send to Wox
|
||||
/// Typically, we will send back this request model to client after user select the result item
|
||||
/// But if the request method starts with "Wox.", we will invoke the public APIs we expose.
|
||||
/// </summary>
|
||||
public class JsonRPCResult : Result
|
||||
{
|
||||
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
|
||||
}
|
||||
}
|
||||
204
src/modules/launcher/Wox.Core/Plugin/JsonRPCPlugin.cs
Normal file
204
src/modules/launcher/Wox.Core/Plugin/JsonRPCPlugin.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Wox.Infrastructure.Exception;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPlugin : IPlugin, IContextMenu
|
||||
{
|
||||
protected PluginInitContext context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
/// <summary>
|
||||
/// The language this JsonRPCPlugin support
|
||||
/// </summary>
|
||||
public abstract string SupportedLanguage { get; set; }
|
||||
|
||||
protected abstract string ExecuteQuery(Query query);
|
||||
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
|
||||
protected abstract string ExecuteContextMenu(Result selectedResult);
|
||||
|
||||
public List<Result> Query(Query query)
|
||||
{
|
||||
string output = ExecuteQuery(query);
|
||||
try
|
||||
{
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
string output = ExecuteContextMenu(selectedResult);
|
||||
try
|
||||
{
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Result> DeserializedResult(string output)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(output))
|
||||
{
|
||||
List<Result> results = new List<Result>();
|
||||
|
||||
JsonRPCQueryResponseModel queryResponseModel = JsonConvert.DeserializeObject<JsonRPCQueryResponseModel>(output);
|
||||
if (queryResponseModel.Result == null) return null;
|
||||
|
||||
foreach (JsonRPCResult result in queryResponseModel.Result)
|
||||
{
|
||||
JsonRPCResult result1 = result;
|
||||
result.Action = c =>
|
||||
{
|
||||
if (result1.JsonRPCAction == null) return false;
|
||||
|
||||
if (!String.IsNullOrEmpty(result1.JsonRPCAction.Method))
|
||||
{
|
||||
if (result1.JsonRPCAction.Method.StartsWith("Wox."))
|
||||
{
|
||||
ExecuteWoxAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
string actionReponse = ExecuteCallback(result1.JsonRPCAction);
|
||||
JsonRPCRequestModel jsonRpcRequestModel = JsonConvert.DeserializeObject<JsonRPCRequestModel>(actionReponse);
|
||||
if (jsonRpcRequestModel != null
|
||||
&& !String.IsNullOrEmpty(jsonRpcRequestModel.Method)
|
||||
&& jsonRpcRequestModel.Method.StartsWith("Wox."))
|
||||
{
|
||||
ExecuteWoxAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
return !result1.JsonRPCAction.DontHideAfterAction;
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteWoxAPI(string method, object[] parameters)
|
||||
{
|
||||
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method);
|
||||
if (methodInfo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
methodInfo.Invoke(PluginManager.API, parameters);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
#if (DEBUG)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute external program and return the output
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="arguments"></param>
|
||||
/// <returns></returns>
|
||||
protected string Execute(string fileName, string arguments)
|
||||
{
|
||||
ProcessStartInfo start = new ProcessStartInfo();
|
||||
start.FileName = fileName;
|
||||
start.Arguments = arguments;
|
||||
start.UseShellExecute = false;
|
||||
start.CreateNoWindow = true;
|
||||
start.RedirectStandardOutput = true;
|
||||
start.RedirectStandardError = true;
|
||||
return Execute(start);
|
||||
}
|
||||
|
||||
protected string Execute(ProcessStartInfo startInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var process = Process.Start(startInfo))
|
||||
{
|
||||
if (process != null)
|
||||
{
|
||||
using (var standardOutput = process.StandardOutput)
|
||||
{
|
||||
var result = standardOutput.ReadToEnd();
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
using (var standardError = process.StandardError)
|
||||
{
|
||||
var error = standardError.ReadToEnd();
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error($"|JsonRPCPlugin.Execute|{error}");
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.StartsWith("DEBUG:"))
|
||||
{
|
||||
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(PluginInitContext ctx)
|
||||
{
|
||||
context = ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/modules/launcher/Wox.Core/Plugin/PluginConfig.cs
Normal file
100
src/modules/launcher/Wox.Core/Plugin/PluginConfig.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Wox.Infrastructure.Exception;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
|
||||
internal abstract class PluginConfig
|
||||
{
|
||||
private const string PluginConfigName = "plugin.json";
|
||||
private static readonly List<PluginMetadata> PluginMetadatas = new List<PluginMetadata>();
|
||||
|
||||
/// <summary>
|
||||
/// Parse plugin metadata in giving directories
|
||||
/// </summary>
|
||||
/// <param name="pluginDirectories"></param>
|
||||
/// <returns></returns>
|
||||
public static List<PluginMetadata> Parse(string[] pluginDirectories)
|
||||
{
|
||||
PluginMetadatas.Clear();
|
||||
var directories = pluginDirectories.SelectMany(Directory.GetDirectories);
|
||||
ParsePluginConfigs(directories);
|
||||
return PluginMetadatas;
|
||||
}
|
||||
|
||||
private static void ParsePluginConfigs(IEnumerable<string> directories)
|
||||
{
|
||||
// todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "NeedDelete.txt")))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginConfig.ParsePLuginConfigs|Can't delete <{directory}>", e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginMetadata metadata = GetPluginMetadata(directory);
|
||||
if (metadata != null)
|
||||
{
|
||||
PluginMetadatas.Add(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PluginMetadata GetPluginMetadata(string pluginDirectory)
|
||||
{
|
||||
string configPath = Path.Combine(pluginDirectory, PluginConfigName);
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
Log.Error($"|PluginConfig.GetPluginMetadata|Didn't find config file <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
PluginMetadata metadata;
|
||||
try
|
||||
{
|
||||
metadata = JsonConvert.DeserializeObject<PluginMetadata>(File.ReadAllText(configPath));
|
||||
metadata.PluginDirectory = pluginDirectory;
|
||||
// for plugins which doesn't has ActionKeywords key
|
||||
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword };
|
||||
// for plugin still use old ActionKeyword
|
||||
metadata.ActionKeyword = metadata.ActionKeywords?[0];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginConfig.GetPluginMetadata|invalid json for config <{configPath}>", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!AllowedLanguage.IsAllowed(metadata.Language))
|
||||
{
|
||||
Log.Error($"|PluginConfig.GetPluginMetadata|Invalid language <{metadata.Language}> for config <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(metadata.ExecuteFilePath))
|
||||
{
|
||||
Log.Error($"|PluginConfig.GetPluginMetadata|execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
202
src/modules/launcher/Wox.Core/Plugin/PluginInstaller.cs
Normal file
202
src/modules/launcher/Wox.Core/Plugin/PluginInstaller.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Newtonsoft.Json;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
internal class PluginInstaller
|
||||
{
|
||||
internal static void Install(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
string tempFoler = Path.Combine(Path.GetTempPath(), "wox\\plugins");
|
||||
if (Directory.Exists(tempFoler))
|
||||
{
|
||||
Directory.Delete(tempFoler, true);
|
||||
}
|
||||
UnZip(path, tempFoler, true);
|
||||
|
||||
string iniPath = Path.Combine(tempFoler, "plugin.json");
|
||||
if (!File.Exists(iniPath))
|
||||
{
|
||||
MessageBox.Show("Install failed: plugin config is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
PluginMetadata plugin = GetMetadataFromJson(tempFoler);
|
||||
if (plugin == null || plugin.Name == null)
|
||||
{
|
||||
MessageBox.Show("Install failed: plugin config is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
string pluginFolerPath = Infrastructure.Constant.PluginsDirectory;
|
||||
|
||||
string newPluginName = plugin.Name
|
||||
.Replace("/", "_")
|
||||
.Replace("\\", "_")
|
||||
.Replace(":", "_")
|
||||
.Replace("<", "_")
|
||||
.Replace(">", "_")
|
||||
.Replace("?", "_")
|
||||
.Replace("*", "_")
|
||||
.Replace("|", "_")
|
||||
+ "-" + Guid.NewGuid();
|
||||
string newPluginPath = Path.Combine(pluginFolerPath, newPluginName);
|
||||
string content = $"Do you want to install following plugin?{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"Name: {plugin.Name}{Environment.NewLine}" +
|
||||
$"Version: {plugin.Version}{Environment.NewLine}" +
|
||||
$"Author: {plugin.Author}";
|
||||
PluginPair existingPlugin = PluginManager.GetPluginForId(plugin.ID);
|
||||
|
||||
if (existingPlugin != null)
|
||||
{
|
||||
content = $"Do you want to update following plugin?{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"Name: {plugin.Name}{Environment.NewLine}" +
|
||||
$"Old Version: {existingPlugin.Metadata.Version}" +
|
||||
$"{Environment.NewLine}New Version: {plugin.Version}" +
|
||||
$"{Environment.NewLine}Author: {plugin.Author}";
|
||||
}
|
||||
|
||||
var result = MessageBox.Show(content, "Install plugin", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
if (existingPlugin != null && Directory.Exists(existingPlugin.Metadata.PluginDirectory))
|
||||
{
|
||||
//when plugin is in use, we can't delete them. That's why we need to make plugin folder a random name
|
||||
File.Create(Path.Combine(existingPlugin.Metadata.PluginDirectory, "NeedDelete.txt")).Close();
|
||||
}
|
||||
|
||||
UnZip(path, newPluginPath, true);
|
||||
Directory.Delete(tempFoler, true);
|
||||
|
||||
//exsiting plugins may be has loaded by application,
|
||||
//if we try to delelte those kind of plugins, we will get a error that indicate the
|
||||
//file is been used now.
|
||||
//current solution is to restart wox. Ugly.
|
||||
//if (MainWindow.Initialized)
|
||||
//{
|
||||
// Plugins.Initialize();
|
||||
//}
|
||||
if (MessageBox.Show($"You have installed plugin {plugin.Name} successfully.{Environment.NewLine}" +
|
||||
"Restart Wox to take effect?",
|
||||
"Install plugin", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
{
|
||||
PluginManager.API.RestarApp();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PluginMetadata GetMetadataFromJson(string pluginDirectory)
|
||||
{
|
||||
string configPath = Path.Combine(pluginDirectory, "plugin.json");
|
||||
PluginMetadata metadata;
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
metadata = JsonConvert.DeserializeObject<PluginMetadata>(File.ReadAllText(configPath));
|
||||
metadata.PluginDirectory = pluginDirectory;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
string error = $"Parse plugin config {configPath} failed: json format is not valid";
|
||||
#if (DEBUG)
|
||||
{
|
||||
throw new Exception(error);
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!AllowedLanguage.IsAllowed(metadata.Language))
|
||||
{
|
||||
string error = $"Parse plugin config {configPath} failed: invalid language {metadata.Language}";
|
||||
#if (DEBUG)
|
||||
{
|
||||
throw new Exception(error);
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
if (!File.Exists(metadata.ExecuteFilePath))
|
||||
{
|
||||
string error = $"Parse plugin config {configPath} failed: ExecuteFile {metadata.ExecuteFilePath} didn't exist";
|
||||
#if (DEBUG)
|
||||
{
|
||||
throw new Exception(error);
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// unzip
|
||||
/// </summary>
|
||||
/// <param name="zipedFile">The ziped file.</param>
|
||||
/// <param name="strDirectory">The STR directory.</param>
|
||||
/// <param name="overWrite">overwirte</param>
|
||||
private static void UnZip(string zipedFile, string strDirectory, bool overWrite)
|
||||
{
|
||||
if (strDirectory == "")
|
||||
strDirectory = Directory.GetCurrentDirectory();
|
||||
if (!strDirectory.EndsWith("\\"))
|
||||
strDirectory = strDirectory + "\\";
|
||||
|
||||
using (ZipInputStream s = new ZipInputStream(File.OpenRead(zipedFile)))
|
||||
{
|
||||
ZipEntry theEntry;
|
||||
|
||||
while ((theEntry = s.GetNextEntry()) != null)
|
||||
{
|
||||
string directoryName = "";
|
||||
string pathToZip = "";
|
||||
pathToZip = theEntry.Name;
|
||||
|
||||
if (pathToZip != "")
|
||||
directoryName = Path.GetDirectoryName(pathToZip) + "\\";
|
||||
|
||||
string fileName = Path.GetFileName(pathToZip);
|
||||
|
||||
Directory.CreateDirectory(strDirectory + directoryName);
|
||||
|
||||
if (fileName != "")
|
||||
{
|
||||
if ((File.Exists(strDirectory + directoryName + fileName) && overWrite) || (!File.Exists(strDirectory + directoryName + fileName)))
|
||||
{
|
||||
using (FileStream streamWriter = File.Create(strDirectory + directoryName + fileName))
|
||||
{
|
||||
byte[] data = new byte[2048];
|
||||
while (true)
|
||||
{
|
||||
int size = s.Read(data, 0, data.Length);
|
||||
|
||||
if (size > 0)
|
||||
streamWriter.Write(data, 0, size);
|
||||
else
|
||||
break;
|
||||
}
|
||||
streamWriter.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
313
src/modules/launcher/Wox.Core/Plugin/PluginManager.cs
Normal file
313
src/modules/launcher/Wox.Core/Plugin/PluginManager.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Infrastructure.Storage;
|
||||
using Wox.Infrastructure.UserSettings;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry for managing Wox plugins
|
||||
/// </summary>
|
||||
public static class PluginManager
|
||||
{
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Wox plugin directory
|
||||
/// </summary>
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly List<PluginPair> GlobalPlugins = new List<PluginPair>();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new Dictionary<string, PluginPair>();
|
||||
|
||||
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<PluginMetadata> _metadatas;
|
||||
private static readonly string[] Directories = { Constant.PreinstalledDirectory, Constant.PluginsDirectory };
|
||||
|
||||
private static void ValidateUserDirectory()
|
||||
{
|
||||
if (!Directory.Exists(Constant.PluginsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(Constant.PluginsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
const string binding = "wox.py";
|
||||
var directory = Constant.PluginsDirectory;
|
||||
foreach (var subDirectory in Directory.GetDirectories(directory))
|
||||
{
|
||||
var path = Path.Combine(subDirectory, binding);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
foreach (var plugin in AllPlugins)
|
||||
{
|
||||
var savable = plugin.Plugin as ISavable;
|
||||
savable?.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReloadData()
|
||||
{
|
||||
foreach(var plugin in AllPlugins)
|
||||
{
|
||||
var reloadablePlugin = plugin.Plugin as IReloadable;
|
||||
reloadablePlugin?.ReloadData();
|
||||
}
|
||||
}
|
||||
|
||||
static PluginManager()
|
||||
{
|
||||
ValidateUserDirectory();
|
||||
// force old plugins use new python binding
|
||||
DeletePythonBinding();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
|
||||
/// todo happlebao The API should be removed
|
||||
/// </summary>
|
||||
/// <param name="settings"></param>
|
||||
public static void LoadPlugins(PluginsSettings settings)
|
||||
{
|
||||
_metadatas = PluginConfig.Parse(Directories);
|
||||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(_metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call initialize for all plugins
|
||||
/// </summary>
|
||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||
public static void InitializePlugins(IPublicAPI api)
|
||||
{
|
||||
API = api;
|
||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||
Parallel.ForEach(AllPlugins, pair =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () =>
|
||||
{
|
||||
pair.Plugin.Init(new PluginInitContext
|
||||
{
|
||||
CurrentPluginMetadata = pair.Metadata,
|
||||
API = API
|
||||
});
|
||||
});
|
||||
pair.Metadata.InitTime += milliseconds;
|
||||
Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e);
|
||||
pair.Metadata.Disabled = true;
|
||||
failedPlugins.Enqueue(pair);
|
||||
}
|
||||
});
|
||||
|
||||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
foreach (var plugin in AllPlugins)
|
||||
{
|
||||
if (IsGlobalPlugin(plugin.Metadata))
|
||||
GlobalPlugins.Add(plugin);
|
||||
|
||||
// Plugins may have multiple ActionKeywords, eg. WebSearch
|
||||
plugin.Metadata.ActionKeywords.Where(x => x != Query.GlobalPluginWildcardSign)
|
||||
.ToList()
|
||||
.ForEach(x => NonGlobalPlugins[x] = plugin);
|
||||
}
|
||||
|
||||
if (failedPlugins.Any())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void InstallPlugin(string path)
|
||||
{
|
||||
PluginInstaller.Install(path);
|
||||
}
|
||||
|
||||
public static List<PluginPair> ValidPluginsForQuery(Query query)
|
||||
{
|
||||
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||
{
|
||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||
return new List<PluginPair> { plugin };
|
||||
}
|
||||
else
|
||||
{
|
||||
return GlobalPlugins;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Result> QueryForPlugin(PluginPair pair, Query query)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Result> results = null;
|
||||
var metadata = pair.Metadata;
|
||||
var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () =>
|
||||
{
|
||||
results = pair.Plugin.Query(query) ?? new List<Result>();
|
||||
UpdatePluginMetadata(results, metadata, query);
|
||||
});
|
||||
metadata.QueryCount += 1;
|
||||
metadata.AvgQueryTime = metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2;
|
||||
return results;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
|
||||
return new List<Result>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdatePluginMetadata(List<Result> results, PluginMetadata metadata, Query query)
|
||||
{
|
||||
foreach (var r in results)
|
||||
{
|
||||
r.PluginDirectory = metadata.PluginDirectory;
|
||||
r.PluginID = metadata.ID;
|
||||
r.OriginQuery = query;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsGlobalPlugin(PluginMetadata metadata)
|
||||
{
|
||||
return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get specified plugin, return null if not found
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public static PluginPair GetPluginForId(string id)
|
||||
{
|
||||
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||
{
|
||||
return AllPlugins.Where(p => p.Plugin is T);
|
||||
}
|
||||
|
||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||
{
|
||||
var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID);
|
||||
if (pluginPair != null)
|
||||
{
|
||||
var metadata = pluginPair.Metadata;
|
||||
var plugin = (IContextMenu)pluginPair.Plugin;
|
||||
|
||||
try
|
||||
{
|
||||
var results = plugin.LoadContextMenus(result);
|
||||
foreach (var r in results)
|
||||
{
|
||||
r.PluginDirectory = metadata.PluginDirectory;
|
||||
r.PluginID = metadata.ID;
|
||||
r.OriginQuery = result.OriginQuery;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{metadata.Name}>", e);
|
||||
return new List<Result>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
||||
{
|
||||
if (actionKeyword != Query.GlobalPluginWildcardSign &&
|
||||
NonGlobalPlugins.ContainsKey(actionKeyword))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// used to add action keyword for multiple action keyword plugin
|
||||
/// e.g. web search
|
||||
/// </summary>
|
||||
public static void AddActionKeyword(string id, string newActionKeyword)
|
||||
{
|
||||
var plugin = GetPluginForId(id);
|
||||
if (newActionKeyword == Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
GlobalPlugins.Add(plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||
}
|
||||
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// used to add action keyword for multiple action keyword plugin
|
||||
/// e.g. web search
|
||||
/// </summary>
|
||||
public static void RemoveActionKeyword(string id, string oldActionkeyword)
|
||||
{
|
||||
var plugin = GetPluginForId(id);
|
||||
if (oldActionkeyword == Query.GlobalPluginWildcardSign
|
||||
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
|
||||
plugin.Metadata.ActionKeywords
|
||||
.Where(x => x == Query.GlobalPluginWildcardSign)
|
||||
.ToList()
|
||||
.Count == 1)
|
||||
{
|
||||
GlobalPlugins.Remove(plugin);
|
||||
}
|
||||
|
||||
if(oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||
|
||||
|
||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||
}
|
||||
|
||||
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
|
||||
{
|
||||
if (oldActionKeyword != newActionKeyword)
|
||||
{
|
||||
AddActionKeyword(id, newActionKeyword);
|
||||
RemoveActionKeyword(id, oldActionKeyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/modules/launcher/Wox.Core/Plugin/PluginsLoader.cs
Normal file
152
src/modules/launcher/Wox.Core/Plugin/PluginsLoader.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Infrastructure.Exception;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Infrastructure.UserSettings;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
public static class PluginsLoader
|
||||
{
|
||||
public const string PATH = "PATH";
|
||||
public const string Python = "python";
|
||||
public const string PythonExecutable = "pythonw.exe";
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var csharpPlugins = CSharpPlugins(metadatas).ToList();
|
||||
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
|
||||
var executablePlugins = ExecutablePlugins(metadatas);
|
||||
var plugins = csharpPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> CSharpPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var plugins = new List<PluginPair>();
|
||||
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.CSharp);
|
||||
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
var milliseconds = Stopwatch.Debug($"|PluginsLoader.CSharpPlugins|Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
|
||||
#if DEBUG
|
||||
var assembly = Assembly.Load(AssemblyName.GetAssemblyName(metadata.ExecuteFilePath));
|
||||
var types = assembly.GetTypes();
|
||||
var type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin)));
|
||||
var plugin = (IPlugin)Activator.CreateInstance(type);
|
||||
#else
|
||||
Assembly assembly;
|
||||
try
|
||||
{
|
||||
assembly = Assembly.Load(AssemblyName.GetAssemblyName(metadata.ExecuteFilePath));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginsLoader.CSharpPlugins|Couldn't load assembly for {metadata.Name}", e);
|
||||
return;
|
||||
}
|
||||
var types = assembly.GetTypes();
|
||||
Type type;
|
||||
try
|
||||
{
|
||||
type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin)));
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Log.Exception($"|PluginsLoader.CSharpPlugins|Can't find class implement IPlugin for <{metadata.Name}>", e);
|
||||
return;
|
||||
}
|
||||
IPlugin plugin;
|
||||
try
|
||||
{
|
||||
plugin = (IPlugin)Activator.CreateInstance(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginsLoader.CSharpPlugins|Can't create instance for <{metadata.Name}>", e);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
PluginPair pair = new PluginPair
|
||||
{
|
||||
Plugin = plugin,
|
||||
Metadata = metadata
|
||||
};
|
||||
plugins.Add(pair);
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirecotry)
|
||||
{
|
||||
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.Python);
|
||||
string filename;
|
||||
|
||||
if (string.IsNullOrEmpty(pythonDirecotry))
|
||||
{
|
||||
var paths = Environment.GetEnvironmentVariable(PATH);
|
||||
if (paths != null)
|
||||
{
|
||||
var pythonPaths = paths.Split(';').Where(p => p.ToLower().Contains(Python));
|
||||
if (pythonPaths.Any())
|
||||
{
|
||||
filename = PythonExecutable;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH.");
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set.");
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var path = Path.Combine(pythonDirecotry, PythonExecutable);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
filename = path;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("|PluginsLoader.PythonPlugins|Can't find python executable in <b ");
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
}
|
||||
Constant.PythonPath = filename;
|
||||
var plugins = metadatas.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new PythonPlugin(filename),
|
||||
Metadata = metadata
|
||||
});
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable);
|
||||
|
||||
var plugins = metadatas.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
});
|
||||
return plugins;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
64
src/modules/launcher/Wox.Core/Plugin/PythonPlugin.cs
Normal file
64
src/modules/launcher/Wox.Core/Plugin/PythonPlugin.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
internal class PythonPlugin : JsonRPCPlugin
|
||||
{
|
||||
private readonly ProcessStartInfo _startInfo;
|
||||
public override string SupportedLanguage { get; set; } = AllowedLanguage.Python;
|
||||
|
||||
public PythonPlugin(string filename)
|
||||
{
|
||||
_startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = filename,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
// temp fix for issue #667
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
|
||||
}
|
||||
|
||||
protected override string ExecuteQuery(Query query)
|
||||
{
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
|
||||
{
|
||||
Method = "query",
|
||||
Parameters = new object[] { query.Search },
|
||||
};
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
|
||||
// todo happlebao why context can't be used in constructor
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
|
||||
{
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
protected override string ExecuteContextMenu(Result selectedResult) {
|
||||
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
|
||||
Method = "context_menu",
|
||||
Parameters = new object[] { selectedResult.ContextData },
|
||||
};
|
||||
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs
Normal file
50
src/modules/launcher/Wox.Core/Plugin/QueryBuilder.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Plugin
|
||||
{
|
||||
public static class QueryBuilder
|
||||
{
|
||||
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
{
|
||||
// replace multiple white spaces with one white space
|
||||
var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (terms.Length == 0)
|
||||
{ // nothing was typed
|
||||
return null;
|
||||
}
|
||||
|
||||
var rawQuery = string.Join(Query.TermSeperater, terms);
|
||||
string actionKeyword, search;
|
||||
string possibleActionKeyword = terms[0];
|
||||
List<string> actionParameters;
|
||||
if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
|
||||
{ // use non global plugin for query
|
||||
actionKeyword = possibleActionKeyword;
|
||||
actionParameters = terms.Skip(1).ToList();
|
||||
search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty;
|
||||
}
|
||||
else
|
||||
{ // non action keyword
|
||||
actionKeyword = string.Empty;
|
||||
actionParameters = terms.ToList();
|
||||
search = rawQuery;
|
||||
}
|
||||
|
||||
var query = new Query
|
||||
{
|
||||
Terms = terms,
|
||||
RawQuery = rawQuery,
|
||||
ActionKeyword = actionKeyword,
|
||||
Search = search,
|
||||
// Obsolete value initialisation
|
||||
ActionName = actionKeyword,
|
||||
ActionParameters = actionParameters
|
||||
};
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/modules/launcher/Wox.Core/Plugin/README.md
Normal file
5
src/modules/launcher/Wox.Core/Plugin/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
There are two kinds of plugins:
|
||||
1. System plugin
|
||||
Those plugins that action keyword is "*", those plugin doesn't need action keyword
|
||||
2. User Plugin
|
||||
Those plugins that contains customized action keyword
|
||||
5
src/modules/launcher/Wox.Core/Properties/AssemblyInfo.cs
Normal file
5
src/modules/launcher/Wox.Core/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("Wox.Core")]
|
||||
[assembly: Guid("693aa0e5-741b-4759-b740-fdbb011a3280")]
|
||||
9
src/modules/launcher/Wox.Core/README.md
Normal file
9
src/modules/launcher/Wox.Core/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
What does Wox.Core do?
|
||||
=====
|
||||
|
||||
* Handle Query
|
||||
* Define Wox exceptions
|
||||
* Manage Plugins (including system plugin and user plugin)
|
||||
* Manage Themes
|
||||
* Manage i18n
|
||||
* Manage Update and version
|
||||
52
src/modules/launcher/Wox.Core/Resource/AvailableLanguages.cs
Normal file
52
src/modules/launcher/Wox.Core/Resource/AvailableLanguages.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
internal static class AvailableLanguages
|
||||
{
|
||||
public static Language English = new Language("en", "English");
|
||||
public static Language Chinese = new Language("zh-cn", "中文");
|
||||
public static Language Chinese_TW = new Language("zh-tw", "中文(繁体)");
|
||||
public static Language Ukrainian = new Language("uk-UA", "Українська");
|
||||
public static Language Russian = new Language("ru", "Русский");
|
||||
public static Language French = new Language("fr", "Français");
|
||||
public static Language Japanese = new Language("ja", "日本語");
|
||||
public static Language Dutch = new Language("nl", "Dutch");
|
||||
public static Language Polish = new Language("pl", "Polski");
|
||||
public static Language Danish = new Language("da", "Dansk");
|
||||
public static Language German = new Language("de", "Deutsch");
|
||||
public static Language Korean = new Language("ko", "한국어");
|
||||
public static Language Serbian = new Language("sr", "Srpski");
|
||||
public static Language Portuguese_BR = new Language("pt-br", "Português (Brasil)");
|
||||
public static Language Italian = new Language("it", "Italiano");
|
||||
public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål");
|
||||
public static Language Slovak = new Language("sk", "Slovenský");
|
||||
public static Language Turkish = new Language("tr", "Türkçe");
|
||||
|
||||
public static List<Language> GetAvailableLanguages()
|
||||
{
|
||||
List<Language> languages = new List<Language>
|
||||
{
|
||||
English,
|
||||
Chinese,
|
||||
Chinese_TW,
|
||||
Ukrainian,
|
||||
Russian,
|
||||
French,
|
||||
Japanese,
|
||||
Dutch,
|
||||
Polish,
|
||||
Danish,
|
||||
German,
|
||||
Korean,
|
||||
Serbian,
|
||||
Portuguese_BR,
|
||||
Italian,
|
||||
Norwegian_Bokmal,
|
||||
Slovak,
|
||||
Turkish
|
||||
};
|
||||
return languages;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/modules/launcher/Wox.Core/Resource/FontHelper.cs
Normal file
73
src/modules/launcher/Wox.Core/Resource/FontHelper.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public static class FontHelper
|
||||
{
|
||||
static FontWeightConverter fontWeightConverter = new FontWeightConverter();
|
||||
public static FontWeight GetFontWeightFromInvariantStringOrNormal(string value)
|
||||
{
|
||||
if (value == null) return FontWeights.Normal;
|
||||
|
||||
try
|
||||
{
|
||||
return (FontWeight) fontWeightConverter.ConvertFromInvariantString(value);
|
||||
}
|
||||
catch {
|
||||
return FontWeights.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
static FontStyleConverter fontStyleConverter = new FontStyleConverter();
|
||||
public static FontStyle GetFontStyleFromInvariantStringOrNormal(string value)
|
||||
{
|
||||
if (value == null) return FontStyles.Normal;
|
||||
|
||||
try
|
||||
{
|
||||
return (FontStyle)fontStyleConverter.ConvertFromInvariantString(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return FontStyles.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
static FontStretchConverter fontStretchConverter = new FontStretchConverter();
|
||||
public static FontStretch GetFontStretchFromInvariantStringOrNormal(string value)
|
||||
{
|
||||
if (value == null) return FontStretches.Normal;
|
||||
try
|
||||
{
|
||||
return (FontStretch)fontStretchConverter.ConvertFromInvariantString(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return FontStretches.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
public static FamilyTypeface ChooseRegularFamilyTypeface(this FontFamily family)
|
||||
{
|
||||
return family.FamilyTypefaces.OrderBy(o =>
|
||||
{
|
||||
return Math.Abs(o.Stretch.ToOpenTypeStretch() - FontStretches.Normal.ToOpenTypeStretch()) * 100 +
|
||||
Math.Abs(o.Weight.ToOpenTypeWeight() - FontWeights.Normal.ToOpenTypeWeight()) +
|
||||
(o.Style == FontStyles.Normal ? 0 : o.Style == FontStyles.Oblique ? 1 : 2) * 1000;
|
||||
}).FirstOrDefault() ?? family.FamilyTypefaces.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static FamilyTypeface ConvertFromInvariantStringsOrNormal(this FontFamily family, string style, string weight, string stretch)
|
||||
{
|
||||
var styleObj = GetFontStyleFromInvariantStringOrNormal(style);
|
||||
var weightObj = GetFontWeightFromInvariantStringOrNormal(weight);
|
||||
var stretchObj = GetFontStretchFromInvariantStringOrNormal(stretch);
|
||||
return family.FamilyTypefaces.FirstOrDefault(o => o.Style == styleObj && o.Weight == weightObj && o.Stretch == stretchObj)
|
||||
?? family.ChooseRegularFamilyTypeface();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
201
src/modules/launcher/Wox.Core/Resource/Internationalization.cs
Normal file
201
src/modules/launcher/Wox.Core/Resource/Internationalization.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using Wox.Core.Plugin;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Infrastructure.UserSettings;
|
||||
using Wox.Plugin;
|
||||
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public class Internationalization
|
||||
{
|
||||
public Settings Settings { get; set; }
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
private const string Extension = ".xaml";
|
||||
private readonly List<string> _languageDirectories = new List<string>();
|
||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||
|
||||
public Internationalization()
|
||||
{
|
||||
AddPluginLanguageDirectories();
|
||||
LoadDefaultLanguage();
|
||||
// we don't want to load /Languages/en.xaml twice
|
||||
// so add wox language directory after load plugin language files
|
||||
AddWoxLanguageDirectory();
|
||||
}
|
||||
|
||||
|
||||
private void AddWoxLanguageDirectory()
|
||||
{
|
||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
_languageDirectories.Add(directory);
|
||||
}
|
||||
|
||||
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||
var dir = Path.GetDirectoryName(location);
|
||||
if (dir != null)
|
||||
{
|
||||
var pluginThemeDirectory = Path.Combine(dir, Folder);
|
||||
_languageDirectories.Add(pluginThemeDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDefaultLanguage()
|
||||
{
|
||||
LoadLanguage(AvailableLanguages.English);
|
||||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
public void ChangeLanguage(string languageCode)
|
||||
{
|
||||
languageCode = languageCode.NonNull();
|
||||
Language language = GetLanguageByLanguageCode(languageCode);
|
||||
ChangeLanguage(language);
|
||||
}
|
||||
|
||||
private Language GetLanguageByLanguageCode(string languageCode)
|
||||
{
|
||||
var lowercase = languageCode.ToLower();
|
||||
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
|
||||
if (language == null)
|
||||
{
|
||||
Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>");
|
||||
return AvailableLanguages.English;
|
||||
}
|
||||
else
|
||||
{
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeLanguage(Language language)
|
||||
{
|
||||
language = language.NonNull();
|
||||
|
||||
Settings.Language = language.LanguageCode;
|
||||
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
UpdatePluginMetadataTranslations();
|
||||
|
||||
}
|
||||
|
||||
private void RemoveOldLanguageFiles()
|
||||
{
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
foreach (var r in _oldResources)
|
||||
{
|
||||
dicts.Remove(r);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadLanguage(Language language)
|
||||
{
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
var filename = $"{language.LanguageCode}{Extension}";
|
||||
var files = _languageDirectories
|
||||
.Select(d => LanguageFile(d, filename))
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.ToArray();
|
||||
|
||||
if (files.Length > 0)
|
||||
{
|
||||
foreach (var f in files)
|
||||
{
|
||||
var r = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(f, UriKind.Absolute)
|
||||
};
|
||||
dicts.Add(r);
|
||||
_oldResources.Add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Language> LoadAvailableLanguages()
|
||||
{
|
||||
return AvailableLanguages.GetAvailableLanguages();
|
||||
}
|
||||
|
||||
public string GetTranslation(string key)
|
||||
{
|
||||
var translation = Application.Current.TryFindResource(key);
|
||||
if (translation is string)
|
||||
{
|
||||
return translation.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"|Internationalization.GetTranslation|No Translation for key {key}");
|
||||
return $"No Translation for key {key}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePluginMetadataTranslations()
|
||||
{
|
||||
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var pluginI18N = p.Plugin as IPluginI18n;
|
||||
if (pluginI18N == null) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|Internationalization.UpdatePluginMetadataTranslations|Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LanguageFile(string folder, string language)
|
||||
{
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
string path = Path.Combine(folder, language);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>");
|
||||
string english = Path.Combine(folder, DefaultFile);
|
||||
if (File.Exists(english))
|
||||
{
|
||||
return english;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"|Internationalization.LanguageFile|Default English Language path can't be found <{path}>");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public static class InternationalizationManager
|
||||
{
|
||||
private static Internationalization instance;
|
||||
private static object syncObject = new object();
|
||||
|
||||
public static Internationalization Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Internationalization();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/launcher/Wox.Core/Resource/Language.cs
Normal file
18
src/modules/launcher/Wox.Core/Resource/Language.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public class Language
|
||||
{
|
||||
public Language(string code, string display)
|
||||
{
|
||||
LanguageCode = code;
|
||||
Display = display;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// E.g. En or Zh-CN
|
||||
/// </summary>
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
public string Display { get; set; }
|
||||
}
|
||||
}
|
||||
263
src/modules/launcher/Wox.Core/Resource/Theme.cs
Normal file
263
src/modules/launcher/Wox.Core/Resource/Theme.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using Wox.Infrastructure.UserSettings;
|
||||
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public class Theme
|
||||
{
|
||||
private readonly List<string> _themeDirectories = new List<string>();
|
||||
private ResourceDictionary _oldResource;
|
||||
private string _oldTheme;
|
||||
public Settings Settings { get; set; }
|
||||
private const string Folder = "Themes";
|
||||
private const string Extension = ".xaml";
|
||||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
private string UserDirectoryPath => Path.Combine(Constant.DataDirectory, Folder);
|
||||
|
||||
public Theme()
|
||||
{
|
||||
_themeDirectories.Add(DirectoryPath);
|
||||
_themeDirectories.Add(UserDirectoryPath);
|
||||
MakesureThemeDirectoriesExist();
|
||||
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
_oldResource = dicts.First(d =>
|
||||
{
|
||||
var p = d.Source.AbsolutePath;
|
||||
var dir = Path.GetDirectoryName(p).NonNull();
|
||||
var info = new DirectoryInfo(dir);
|
||||
var f = info.Name;
|
||||
var e = Path.GetExtension(p);
|
||||
var found = f == Folder && e == Extension;
|
||||
return found;
|
||||
});
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
private void MakesureThemeDirectoriesExist()
|
||||
{
|
||||
foreach (string dir in _themeDirectories)
|
||||
{
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ChangeTheme(string theme)
|
||||
{
|
||||
const string defaultTheme = "Dark";
|
||||
|
||||
string path = GetThemePath(theme);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
||||
|
||||
Settings.Theme = theme;
|
||||
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
if (_oldTheme != theme || theme == defaultTheme)
|
||||
{
|
||||
dicts.Remove(_oldResource);
|
||||
var newResource = GetResourceDictionary();
|
||||
dicts.Add(newResource);
|
||||
_oldResource = newResource;
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException e)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ResourceDictionary GetResourceDictionary()
|
||||
{
|
||||
var uri = GetThemePath(Settings.Theme);
|
||||
var dict = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(uri, UriKind.Absolute)
|
||||
};
|
||||
|
||||
Style queryBoxStyle = dict["QueryBoxStyle"] as Style;
|
||||
if (queryBoxStyle != null)
|
||||
{
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, new FontFamily(Settings.QueryBoxFont)));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle)));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight)));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch)));
|
||||
}
|
||||
|
||||
Style resultItemStyle = dict["ItemTitleStyle"] as Style;
|
||||
Style resultSubItemStyle = dict["ItemSubTitleStyle"] as Style;
|
||||
Style resultItemSelectedStyle = dict["ItemTitleSelectedStyle"] as Style;
|
||||
Style resultSubItemSelectedStyle = dict["ItemSubTitleSelectedStyle"] as Style;
|
||||
if (resultItemStyle != null && resultSubItemStyle != null && resultSubItemSelectedStyle != null && resultItemSelectedStyle != null)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
public List<string> LoadAvailableThemes()
|
||||
{
|
||||
List<string> themes = new List<string>();
|
||||
foreach (var themeDirectory in _themeDirectories)
|
||||
{
|
||||
themes.AddRange(
|
||||
Directory.GetFiles(themeDirectory)
|
||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||
.ToList());
|
||||
}
|
||||
return themes.OrderBy(o => o).ToList();
|
||||
}
|
||||
|
||||
private string GetThemePath(string themeName)
|
||||
{
|
||||
foreach (string themeDirectory in _themeDirectories)
|
||||
{
|
||||
string path = Path.Combine(themeDirectory, themeName + Extension);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#region Blur Handling
|
||||
/*
|
||||
Found on https://github.com/riverar/sample-win10-aeroglass
|
||||
*/
|
||||
private enum AccentState
|
||||
{
|
||||
ACCENT_DISABLED = 0,
|
||||
ACCENT_ENABLE_GRADIENT = 1,
|
||||
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
|
||||
ACCENT_ENABLE_BLURBEHIND = 3,
|
||||
ACCENT_INVALID_STATE = 4
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AccentPolicy
|
||||
{
|
||||
public AccentState AccentState;
|
||||
public int AccentFlags;
|
||||
public int GradientColor;
|
||||
public int AnimationId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WindowCompositionAttributeData
|
||||
{
|
||||
public WindowCompositionAttribute Attribute;
|
||||
public IntPtr Data;
|
||||
public int SizeOfData;
|
||||
}
|
||||
|
||||
private enum WindowCompositionAttribute
|
||||
{
|
||||
WCA_ACCENT_POLICY = 19
|
||||
}
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the blur for a window via SetWindowCompositionAttribute
|
||||
/// </summary>
|
||||
public void SetBlurForWindow()
|
||||
{
|
||||
|
||||
// Exception of FindResource can't be cathed if global exception handle is set
|
||||
if (Environment.OSVersion.Version >= new Version(6, 2))
|
||||
{
|
||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||
bool blur;
|
||||
if (resource is bool)
|
||||
{
|
||||
blur = (bool)resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
blur = false;
|
||||
}
|
||||
|
||||
if (blur)
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWindowAccent(Window w, AccentState state)
|
||||
{
|
||||
var windowHelper = new WindowInteropHelper(w);
|
||||
var accent = new AccentPolicy { AccentState = state };
|
||||
var accentStructSize = Marshal.SizeOf(accent);
|
||||
|
||||
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
|
||||
Marshal.StructureToPtr(accent, accentPtr, false);
|
||||
|
||||
var data = new WindowCompositionAttributeData
|
||||
{
|
||||
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
|
||||
SizeOfData = accentStructSize,
|
||||
Data = accentPtr
|
||||
};
|
||||
|
||||
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
|
||||
|
||||
Marshal.FreeHGlobal(accentPtr);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
26
src/modules/launcher/Wox.Core/Resource/ThemeManager.cs
Normal file
26
src/modules/launcher/Wox.Core/Resource/ThemeManager.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Wox.Core.Resource
|
||||
{
|
||||
public class ThemeManager
|
||||
{
|
||||
private static Theme instance;
|
||||
private static object syncObject = new object();
|
||||
|
||||
public static Theme Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Theme();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
src/modules/launcher/Wox.Core/Updater.cs
Normal file
146
src/modules/launcher/Wox.Core/Updater.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
using Newtonsoft.Json;
|
||||
using Wox.Core.Resource;
|
||||
using Wox.Plugin.SharedCommands;
|
||||
using Wox.Infrastructure;
|
||||
using Wox.Infrastructure.Http;
|
||||
using Wox.Infrastructure.Logger;
|
||||
using System.IO;
|
||||
|
||||
namespace Wox.Core
|
||||
{
|
||||
public class Updater
|
||||
{
|
||||
public string GitHubRepository { get; }
|
||||
|
||||
public Updater(string gitHubRepository)
|
||||
{
|
||||
GitHubRepository = gitHubRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateApp(bool silentIfLatestVersion = true)
|
||||
{
|
||||
UpdateManager updateManager;
|
||||
UpdateInfo newUpdateInfo;
|
||||
|
||||
try
|
||||
{
|
||||
updateManager = await GitHubUpdateManager(GitHubRepository);
|
||||
}
|
||||
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
|
||||
{
|
||||
Log.Exception($"|Updater.UpdateApp|Please check your connection and proxy settings to api.github.com.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
|
||||
newUpdateInfo = await updateManager.CheckForUpdate().NonNull();
|
||||
}
|
||||
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
|
||||
{
|
||||
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to api.github.com.", e);
|
||||
updateManager.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||
var currentVersion = Version.Parse(Constant.Version);
|
||||
|
||||
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
|
||||
|
||||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
if (!silentIfLatestVersion)
|
||||
MessageBox.Show("You already have the latest Wox version");
|
||||
updateManager.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply);
|
||||
}
|
||||
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
|
||||
{
|
||||
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||
updateManager.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
await updateManager.ApplyReleases(newUpdateInfo);
|
||||
|
||||
if (Constant.IsPortableMode)
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{Constant.PortableFolderName}";
|
||||
FilesFolders.Copy(Constant.PortableDataPath, targetDestination);
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(Constant.PortableDataPath, targetDestination))
|
||||
MessageBox.Show(string.Format("Wox was not able to move your user profile data to the new update version. Please manually" +
|
||||
"move your profile data folder from {0} to {1}", Constant.PortableDataPath, targetDestination));
|
||||
}
|
||||
else
|
||||
{
|
||||
await updateManager.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString());
|
||||
|
||||
MessageBox.Show(newVersionTips);
|
||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||
|
||||
// always dispose UpdateManager
|
||||
updateManager.Dispose();
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private class GithubRelease
|
||||
{
|
||||
[JsonProperty("prerelease")]
|
||||
public bool Prerelease { get; [UsedImplicitly] set; }
|
||||
|
||||
[JsonProperty("published_at")]
|
||||
public DateTime PublishedAt { get; [UsedImplicitly] set; }
|
||||
|
||||
[JsonProperty("html_url")]
|
||||
public string HtmlUrl { get; [UsedImplicitly] set; }
|
||||
}
|
||||
|
||||
/// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManager(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||
|
||||
var json = await Http.Get(api);
|
||||
|
||||
var releases = JsonConvert.DeserializeObject<List<GithubRelease>>(json);
|
||||
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
|
||||
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
|
||||
|
||||
var client = new WebClient { Proxy = Http.WebProxy() };
|
||||
var downloader = new FileDownloader(client);
|
||||
|
||||
var manager = new UpdateManager(latestUrl, urlDownloader: downloader);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
public string NewVersinoTips(string version)
|
||||
{
|
||||
var translater = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translater.GetTranslation("newVersionTips"), version);
|
||||
return tips;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
122
src/modules/launcher/Wox.Core/Wox.Core.csproj
Normal file
122
src/modules/launcher/Wox.Core/Wox.Core.csproj
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B749F0DB-8E75-47DB-9E5E-265D16D0C0D2}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Wox.Core</RootNamespace>
|
||||
<AssemblyName>Wox.Core</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
||||
<TargetFrameworkProfile />
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>..\Output\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>..\Output\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SolutionAssemblyInfo.cs">
|
||||
<Link>Properties\SolutionAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Plugin\ExecutablePlugin.cs" />
|
||||
<Compile Include="Plugin\PluginsLoader.cs" />
|
||||
<Compile Include="Plugin\QueryBuilder.cs" />
|
||||
<Compile Include="Updater.cs" />
|
||||
<Compile Include="Resource\AvailableLanguages.cs" />
|
||||
<Compile Include="Resource\Internationalization.cs" />
|
||||
<Compile Include="Resource\InternationalizationManager.cs" />
|
||||
<Compile Include="Resource\Language.cs" />
|
||||
<Compile Include="Resource\ThemeManager.cs" />
|
||||
<Compile Include="Plugin\PluginInstaller.cs" />
|
||||
<Compile Include="Plugin\JsonRPCPlugin.cs" />
|
||||
<Compile Include="Plugin\JsonPRCModel.cs" />
|
||||
<Compile Include="Plugin\PluginConfig.cs" />
|
||||
<Compile Include="Plugin\PluginManager.cs" />
|
||||
<Compile Include="Plugin\PythonPlugin.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Resource\FontHelper.cs" />
|
||||
<Compile Include="Resource\Theme.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Plugin\README.md" />
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wox.Infrastructure\Wox.Infrastructure.csproj">
|
||||
<Project>{4fd29318-a8ab-4d8f-aa47-60bc241b8da3}</Project>
|
||||
<Name>Wox.Infrastructure</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Wox.Plugin\Wox.Plugin.csproj">
|
||||
<Project>{8451ecdd-2ea4-4966-bb0a-7bbc40138e80}</Project>
|
||||
<Name>Wox.Plugin</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody">
|
||||
<Version>1.29.2</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="JetBrains.Annotations">
|
||||
<Version>10.3.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>9.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="PropertyChanged.Fody">
|
||||
<Version>1.51.0</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="squirrel.windows">
|
||||
<Version>1.5.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Runtime">
|
||||
<Version>4.0.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
Reference in New Issue
Block a user