diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index be5d10eae..ece73691e 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -49,6 +49,7 @@ AModifier
AMPROPERTY
AMPROPSETID
anges
+angularsen
ansicolor
ANull
AOC
@@ -1496,6 +1497,7 @@ ppv
pragma
prc
precomp
+Prefixer
Preinstalled
preload
PREMULTIPLIED
@@ -1744,6 +1746,7 @@ shldisp
shlobj
shlwapi
shobjidl
+shortsplit
SHORTCUTATLEAST
shortcutcontrol
Shortcutguide
@@ -2027,6 +2030,8 @@ uninstantiated
Uniq
uniquifier
Uniquifies
+unitconvert
+unitconverter
unittests
unk
unknwn
diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml
index 84847b592..2b915c4bd 100644
--- a/.pipelines/ci/templates/build-powertoys-steps.yml
+++ b/.pipelines/ci/templates/build-powertoys-steps.yml
@@ -147,6 +147,7 @@ steps:
configuration: '$(BuildConfiguration)'
testSelector: 'testAssemblies'
testAssemblyVer2: |
+ **\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.dll
**\Microsoft.Plugin.Folder.UnitTests.dll
**\Microsoft.Plugin.Program.UnitTests.dll
**\Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest.dll
diff --git a/.pipelines/pipeline.user.windows.yml b/.pipelines/pipeline.user.windows.yml
index d74791fa1..cf82c18ed 100644
--- a/.pipelines/pipeline.user.windows.yml
+++ b/.pipelines/pipeline.user.windows.yml
@@ -112,6 +112,7 @@ build:
- 'modules\launcher\ManagedTelemetry.dll'
- 'modules\launcher\Microsoft.PowerToys.Common.UI.dll'
- 'modules\launcher\Microsoft.Launcher.dll'
+ - 'modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.dll'
- 'modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.dll'
- 'modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator\Wox.Infrastructure.dll'
- 'modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator\Wox.Plugin.dll'
diff --git a/PowerToys.sln b/PowerToys.sln
index 0aa7e6074..10a4e9b81 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -145,14 +145,18 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Launcher", "src\m
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher", "src\modules\launcher\PowerLauncher\PowerLauncher.csproj", "{F97E5003-F263-4D4A-A964-0F1F3C82DEF2}"
ProjectSection(ProjectDependencies) = postProject
+ {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B} = {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}
{03276A39-D4E9-417C-8FFD-200B0EE5E871} = {03276A39-D4E9-417C-8FFD-200B0EE5E871}
+ {4D971245-7A70-41D5-BAA0-DDB5684CAF51} = {4D971245-7A70-41D5-BAA0-DDB5684CAF51}
{FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4}
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}
{59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E}
{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}
{0351ADA4-0C32-4652-9BA0-41F7B602372B} = {0351ADA4-0C32-4652-9BA0-41F7B602372B}
{787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}
{F8B870EB-D5F5-45BA-9CF7-A5C459818820} = {F8B870EB-D5F5-45BA-9CF7-A5C459818820}
{74F1B9ED-F59C-4FE7-B473-7B453E30837E} = {74F1B9ED-F59C-4FE7-B473-7B453E30837E}
+ {4BABF3FE-3451-42FD-873F-3C332E18DCEF} = {4BABF3FE-3451-42FD-873F-3C332E18DCEF}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E775CC2C-24CB-48D6-9C3A-BE4CCE0DB17A}"
@@ -325,8 +329,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorTest",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "espresso", "espresso", "{127F38E0-40AA-4594-B955-5616BF206882}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.csproj", "{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}"
+EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EspressoModuleInterface", "src\modules\espresso\EspressoModuleInterface\EspressoModuleInterface.vcxproj", "{5E7360A8-D048-4ED3-8F09-0BFD64C5529A}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter.UnitTest", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj", "{3E424AD2-19E5-4AE6-B833-F53963EB5FC1}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Espresso", "src\modules\espresso\Espresso\Espresso.csproj", "{D940E07F-532C-4FF3-883F-790DA014F19A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shortcutguide", "shortcutguide", "{106CBECA-0701-4FC3-838C-9DF816A19AE2}"
@@ -675,6 +683,14 @@ Global
{D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.Build.0 = Debug|x64
{D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.ActiveCfg = Release|x64
{D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.Build.0 = Release|x64
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.ActiveCfg = Debug|x64
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.Build.0 = Debug|x64
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.ActiveCfg = Release|x64
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.Build.0 = Release|x64
+ {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.ActiveCfg = Debug|x64
+ {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.Build.0 = Debug|x64
+ {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.ActiveCfg = Release|x64
+ {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.Build.0 = Release|x64
{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.ActiveCfg = Debug|x64
{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.Build.0 = Debug|x64
{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.ActiveCfg = Release|x64
@@ -783,6 +799,8 @@ Global
{62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
{127F38E0-40AA-4594-B955-5616BF206882} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {127F38E0-40AA-4594-B955-5616BF206882}
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
+ {3E424AD2-19E5-4AE6-B833-F53963EB5FC1} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{D940E07F-532C-4FF3-883F-790DA014F19A} = {127F38E0-40AA-4594-B955-5616BF206882}
{106CBECA-0701-4FC3-838C-9DF816A19AE2} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{2D604C07-51FC-46BB-9EB7-75AECC7F5E81} = {106CBECA-0701-4FC3-838C-9DF816A19AE2}
diff --git a/doc/devdocs/modules/launcher/plugins/community.unitconverter.md b/doc/devdocs/modules/launcher/plugins/community.unitconverter.md
new file mode 100644
index 000000000..aff29968b
--- /dev/null
+++ b/doc/devdocs/modules/launcher/plugins/community.unitconverter.md
@@ -0,0 +1,31 @@
+# Unit Converter Plugin
+The Unit Convert plugin as the name suggests is used to perform unit conversion on the user entered query.
+This plugin uses a package called [UnitsNet](https://github.com/angularsen/UnitsNet).
+
+![Image of Calculator plugin](/doc/images/launcher/plugins/community.unitconverter.png)
+
+### Currently Supported Units
+ - [Acceleration](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/AccelerationUnit.g.cs)
+ - [Angle](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/AngleUnit.g.cs)
+ - [Area](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/AreaUnit.g.cs)
+ - [Duration](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/DurationUnit.g.cs)
+ - [Energy](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/EnergyUnit.g.cs)
+ - [Information](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/InformationUnit.g.cs)
+ - [Length](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/LengthUnit.g.cs)
+ - [Mass](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/MassUnit.g.cs)
+ - [Power](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/PowerUnit.g.cs)
+ - [Pressure](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/PressureUnit.g.cs)
+ - [Speed](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/SpeedUnit.g.cs)
+ - [Temperature](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/TemperatureUnit.g.cs)
+ - [Volume](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/VolumeUnit.g.cs)
+
+ These are the ones that are currently enabled (though UnitsNet supports many more). They are defined in [`Main.cs`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/Main.cs).
+
+
+### [`InputInterpreter`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/InputInterpreter.cs)
+ - Class which manipulates user input such that it may be interpreted correctly and thus converted.
+ - Uses a regex amongst other things to do this.
+
+### [`UnitHandler`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/UnitHandler.cs)
+ - Class that does the actual conversion.
+ - Supports abbreviations in user input (single, double, or none).
\ No newline at end of file
diff --git a/doc/images/launcher/plugins/community.unitconverter.png b/doc/images/launcher/plugins/community.unitconverter.png
new file mode 100644
index 000000000..f7bcdbf04
Binary files /dev/null and b/doc/images/launcher/plugins/community.unitconverter.png differ
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index bc8881766..821264acb 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -276,6 +276,10 @@
+
+
+
+
@@ -907,7 +911,7 @@
-
+
@@ -980,6 +984,9 @@
+
+
+
@@ -1015,7 +1022,7 @@
-
+
@@ -1090,6 +1097,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj
new file mode 100644
index 000000000..d30c233c4
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj
@@ -0,0 +1,37 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+ x64
+
+
+
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs
new file mode 100644
index 000000000..0d6dfb427
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using NUnit.Framework;
+using Wox.Plugin;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter.UnitTest
+{
+ [TestFixture]
+ public class InputInterpreterTests
+ {
+ [TestCase(new string[] { "1,5'" }, new string[] { "1,5", "'" })]
+ [TestCase(new string[] { "1.5'" }, new string[] { "1.5", "'" })]
+ [TestCase(new string[] { "1'" }, new string[] { "1", "'" })]
+ [TestCase(new string[] { "1'5\"" }, new string[] { "1", "'", "5", "\"" })]
+ [TestCase(new string[] { "5\"" }, new string[] { "5", "\"" })]
+ [TestCase(new string[] { "1'5" }, new string[] { "1", "'", "5" })]
+ public void RegexSplitsInput(string[] input, string[] expectedResult)
+ {
+ string[] shortsplit = InputInterpreter.RegexSplitter(input);
+ Assert.AreEqual(expectedResult, shortsplit);
+ }
+
+ [TestCase(new string[] { "1cm", "to", "mm" }, new string[] { "1", "cm", "to", "mm" })]
+ public void InsertsSpaces(string[] input, string[] expectedResult)
+ {
+ InputInterpreter.InputSpaceInserter(ref input);
+ Assert.AreEqual(expectedResult, input);
+ }
+
+ [TestCase(new string[] { "1'", "in", "cm" }, new string[] { "1", "foot", "in", "cm" })]
+ [TestCase(new string[] { "1\"", "in", "cm" }, new string[] { "1", "inch", "in", "cm" })]
+ [TestCase(new string[] { "1'6", "in", "cm" }, new string[] { "1.5", "foot", "in", "cm" })]
+ [TestCase(new string[] { "1'6\"", "in", "cm" }, new string[] { "1.5", "foot", "in", "cm" })]
+ public void HandlesShorthandFeetInchNotation(string[] input, string[] expectedResult)
+ {
+ InputInterpreter.ShorthandFeetInchHandler(ref input, CultureInfo.InvariantCulture);
+ Assert.AreEqual(expectedResult, input);
+ }
+
+ [TestCase(new string[] { "5", "CeLsIuS", "in", "faHrenheiT" }, new string[] { "5", "DegreeCelsius", "in", "DegreeFahrenheit" })]
+ [TestCase(new string[] { "5", "f", "in", "celsius" }, new string[] { "5", "°f", "in", "DegreeCelsius" })]
+ [TestCase(new string[] { "5", "c", "in", "f" }, new string[] { "5", "°c", "in", "°f" })]
+ [TestCase(new string[] { "5", "f", "in", "c" }, new string[] { "5", "°f", "in", "°c" })]
+ public void PrefixesDegrees(string[] input, string[] expectedResult)
+ {
+ InputInterpreter.DegreePrefixer(ref input);
+ Assert.AreEqual(expectedResult, input);
+ }
+
+ [TestCase("a f in c")]
+ [TestCase("12 f in")]
+ public void ParseInvalidQueries(string queryString)
+ {
+ Query query = new Query(queryString);
+ var result = InputInterpreter.Parse(query);
+ Assert.AreEqual(null, result);
+ }
+
+ [TestCase("12 f in c", 12)]
+ [TestCase("10m to cm", 10)]
+ public void ParseValidQueries(string queryString, double result)
+ {
+ Query query = new Query(queryString);
+ var convertModel = InputInterpreter.Parse(query);
+ Assert.AreEqual(result, convertModel.Value);
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/UnitHandlerTests.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/UnitHandlerTests.cs
new file mode 100644
index 000000000..aef9210a0
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/UnitHandlerTests.cs
@@ -0,0 +1,42 @@
+using System.Globalization;
+using System.Linq;
+using NUnit.Framework;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter.UnitTest
+{
+ [TestFixture]
+ public class UnitHandlerTests
+ {
+ [Test]
+ public void HandleTemperature()
+ {
+ var convertModel = new ConvertModel(1, "DegreeCelsius", "DegreeFahrenheit");
+ double result = UnitHandler.ConvertInput(convertModel, UnitsNet.QuantityType.Temperature);
+ Assert.AreEqual(33.79999999999999d, result);
+ }
+
+ [Test]
+ public void HandleLength()
+ {
+ var convertModel = new ConvertModel(1, "meter", "centimeter");
+ double result = UnitHandler.ConvertInput(convertModel, UnitsNet.QuantityType.Length);
+ Assert.AreEqual(100, result);
+ }
+
+ [Test]
+ public void HandlesByteCapitals()
+ {
+ var convertModel = new ConvertModel(1, "kB", "kb");
+ double result = UnitHandler.ConvertInput(convertModel, UnitsNet.QuantityType.Information);
+ Assert.AreEqual(8, result);
+ }
+
+ [Test]
+ public void HandleInvalidModel()
+ {
+ var convertModel = new ConvertModel(1, "aa", "bb");
+ var results = UnitHandler.Convert(convertModel);
+ Assert.AreEqual(0, results.Count());
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Community.PowerToys.Run.Plugin.UnitConverter.csproj b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Community.PowerToys.Run.Plugin.UnitConverter.csproj
new file mode 100644
index 000000000..fe32784f5
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Community.PowerToys.Run.Plugin.UnitConverter.csproj
@@ -0,0 +1,99 @@
+
+
+
+
+ netcoreapp3.1
+ {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}
+ Properties
+ Community.PowerToys.Run.Plugin.UnitConverter
+ Community.PowerToys.Run.Plugin.UnitConverter
+ $(Version).0
+ true
+ false
+ false
+ x64
+ en-US
+
+
+
+ true
+ ..\..\..\..\..\x64\Debug\modules\launcher\Plugins\Community.UnitConverter\
+ DEBUG;TRACE
+ full
+ x64
+ 7.3
+ prompt
+ MinimumRecommendedRules.ruleset
+ 4
+ false
+ false
+
+
+
+ ..\..\..\..\..\x64\Release\modules\launcher\Plugins\Community.UnitConverter\
+ TRACE
+ true
+ pdbonly
+ x64
+ 7.3
+ prompt
+ MinimumRecommendedRules.ruleset
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+ false
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/ConvertModel.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/ConvertModel.cs
new file mode 100644
index 000000000..e82ee9ae0
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/ConvertModel.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter
+{
+ public class ConvertModel
+ {
+ public double Value { get; set; }
+
+ public string FromUnit { get; set; }
+
+ public string ToUnit { get; set; }
+
+ public ConvertModel()
+ {
+ }
+
+ public ConvertModel(double value, string fromUnit, string toUnit)
+ {
+ Value = value;
+ FromUnit = fromUnit;
+ ToUnit = toUnit;
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.dark.png b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.dark.png
new file mode 100644
index 000000000..e2553f953
Binary files /dev/null and b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.dark.png differ
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.light.png b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.light.png
new file mode 100644
index 000000000..1804d66a1
Binary files /dev/null and b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Images/unitconverter.light.png differ
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs
new file mode 100644
index 000000000..b44aa8022
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.RegularExpressions;
+using UnitsNet;
+using Wox.Plugin;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter
+{
+ public static class InputInterpreter
+ {
+ private static string pattern = @"(?<=\d)(?![,.])(?=\D)|(?<=\D)(?
+ /// Separates input like: "1ft in cm" to "1 ft in cm"
+ ///
+ public static void InputSpaceInserter(ref string[] split)
+ {
+ if (split.Length != 3)
+ {
+ return;
+ }
+
+ string[] parseInputWithoutSpace = Regex.Split(split[0], pattern);
+
+ if (parseInputWithoutSpace.Length > 1)
+ {
+ string[] firstEntryRemoved = split.Skip(1).ToArray();
+ string[] newSplit = new string[] { parseInputWithoutSpace[0], parseInputWithoutSpace[1] };
+
+ split = newSplit.Concat(firstEntryRemoved).ToArray();
+ }
+ }
+
+ ///
+ /// Replaces a split input array with shorthand feet/inch notation (1', 1'2" etc) to 'x foot in cm'.
+ ///
+ public static void ShorthandFeetInchHandler(ref string[] split, CultureInfo culture)
+ {
+ if (!split[0].Contains('\'') && !split[0].Contains('\"'))
+ {
+ return;
+ }
+
+ // catches 1' || 1" || 1'2 || 1'2" in cm
+ // by converting it to "x foot in cm"
+ if (split.Length == 3)
+ {
+ string[] shortsplit = RegexSplitter(split);
+
+ switch (shortsplit.Length)
+ {
+ case 2:
+ // ex: 1' & 1"
+ if (shortsplit[1] == "\'")
+ {
+ string[] newInput = new string[] { shortsplit[0], "foot", split[1], split[2] };
+ split = newInput;
+ }
+ else if (shortsplit[1] == "\"")
+ {
+ string[] newInput = new string[] { shortsplit[0], "inch", split[1], split[2] };
+ split = newInput;
+ }
+
+ break;
+
+ case 3:
+ case 4:
+ // ex: 1'2 and 1'2"
+ if (shortsplit[1] == "\'")
+ {
+ bool isFeet = double.TryParse(shortsplit[0], NumberStyles.AllowDecimalPoint, culture, out double feet);
+ bool isInches = double.TryParse(shortsplit[2], NumberStyles.AllowDecimalPoint, culture, out double inches);
+
+ if (!isFeet || !isInches)
+ {
+ // atleast one could not be parsed correctly
+ break;
+ }
+
+ string convertedTotalInFeet = Length.FromFeetInches(feet, inches).Feet.ToString(culture);
+
+ string[] newInput = new string[] { convertedTotalInFeet, "foot", split[1], split[2] };
+ split = newInput;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Adds degree prefixes to degree units for shorthand notation. E.g. '10 c in fahrenheit' becomes '10 °c in DegreeFahrenheit'.
+ ///
+ public static void DegreePrefixer(ref string[] split)
+ {
+ switch (split[1].ToLower())
+ {
+ case "celsius":
+ split[1] = "DegreeCelsius";
+ break;
+
+ case "fahrenheit":
+ split[1] = "DegreeFahrenheit";
+ break;
+
+ case "c":
+ split[1] = "°c";
+ break;
+
+ case "f":
+ split[1] = "°f";
+ break;
+
+ default:
+ break;
+ }
+
+ switch (split[3].ToLower())
+ {
+ case "celsius":
+ split[3] = "DegreeCelsius";
+ break;
+
+ case "fahrenheit":
+ split[3] = "DegreeFahrenheit";
+ break;
+
+ case "c":
+ split[3] = "°c";
+ break;
+
+ case "f":
+ split[3] = "°f";
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ public static ConvertModel Parse(Query query)
+ {
+ string[] split = query.Search.Split(' ');
+
+ InputInterpreter.ShorthandFeetInchHandler(ref split, CultureInfo.CurrentCulture);
+ InputInterpreter.InputSpaceInserter(ref split);
+
+ if (split.Length != 4)
+ {
+ // deny any other queries than:
+ // 10 ft in cm
+ // 10 ft to cm
+ return null;
+ }
+
+ InputInterpreter.DegreePrefixer(ref split);
+ if (!double.TryParse(split[0], out double value))
+ {
+ return null;
+ }
+
+ return new ConvertModel()
+ {
+ Value = value,
+ FromUnit = split[1],
+ ToUnit = split[3],
+ };
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/LocProject.json b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/LocProject.json
new file mode 100644
index 000000000..4976e045c
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/LocProject.json
@@ -0,0 +1,14 @@
+{
+ "Projects": [
+ {
+ "LanguageSet": "Azure_Languages",
+ "LocItems": [
+ {
+ "SourceFile": "src\\modules\\launcher\\Plugins\\Community.PowerToys.Run.Plugin.UnitConverter\\Properties\\Resources.resx",
+ "CopyOption": "LangIDOnName",
+ "OutputPath": "src\\modules\\launcher\\Plugins\\Community.PowerToys.Run.Plugin.UnitConverter\\Properties"
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Main.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Main.cs
new file mode 100644
index 000000000..2c5985969
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Main.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Windows;
+using System.Windows.Input;
+using ManagedCommon;
+using UnitsNet;
+using Wox.Plugin;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter
+{
+ public class Main : IPlugin, IPluginI18n, IContextMenu, IDisposable
+ {
+ public string Name => Properties.Resources.plugin_name;
+
+ public string Description => Properties.Resources.plugin_description;
+
+ private PluginInitContext _context;
+ private static string _icon_path;
+ private bool _disposed;
+
+ public void Init(PluginInitContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(paramName: nameof(context));
+ }
+
+ _context = context;
+ _context.API.ThemeChanged += OnThemeChanged;
+ UpdateIconPath(_context.API.GetCurrentTheme());
+ }
+
+ public List Query(Query query)
+ {
+ if (query == null)
+ {
+ throw new ArgumentNullException(paramName: nameof(query));
+ }
+
+ // Parse
+ ConvertModel convertModel = InputInterpreter.Parse(query);
+ if (convertModel == null)
+ {
+ return new List();
+ }
+
+ // Convert
+ return UnitHandler.Convert(convertModel)
+ .Select(x => GetResult(x))
+ .ToList();
+ }
+
+ private Result GetResult(UnitConversionResult result)
+ {
+ return new Result
+ {
+ ContextData = result,
+ Title = string.Format("{0} {1}", result.ConvertedValue, result.UnitName),
+ IcoPath = _icon_path,
+ Score = 300,
+ SubTitle = string.Format(Properties.Resources.copy_to_clipboard, result.QuantityType),
+ Action = c =>
+ {
+ var ret = false;
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ Clipboard.SetText(result.ConvertedValue.ToString());
+ ret = true;
+ }
+ catch (ExternalException)
+ {
+ MessageBox.Show(Properties.Resources.copy_failed);
+ }
+ });
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+ return ret;
+ },
+ };
+ }
+
+ private ContextMenuResult CreateContextMenuEntry(UnitConversionResult result)
+ {
+ return new ContextMenuResult
+ {
+ PluginName = Name,
+ Title = Properties.Resources.context_menu_copy,
+ Glyph = "\xE8C8",
+ FontFamily = "Segoe MDL2 Assets",
+ AcceleratorKey = Key.Enter,
+ Action = _ =>
+ {
+ bool ret = false;
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ Clipboard.SetText(result.ConvertedValue.ToString());
+ ret = true;
+ }
+ catch (ExternalException)
+ {
+ MessageBox.Show(Properties.Resources.copy_failed);
+ }
+ });
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+ return ret;
+ },
+ };
+ }
+
+ public List LoadContextMenus(Result selectedResult)
+ {
+ if (!(selectedResult?.ContextData is UnitConversionResult))
+ {
+ return new List();
+ }
+
+ List contextResults = new List();
+ UnitConversionResult result = selectedResult.ContextData as UnitConversionResult;
+ contextResults.Add(CreateContextMenuEntry(result));
+
+ return contextResults;
+ }
+
+ public string GetTranslatedPluginTitle()
+ {
+ return Properties.Resources.plugin_name;
+ }
+
+ public string GetTranslatedPluginDescription()
+ {
+ return Properties.Resources.plugin_description;
+ }
+
+ private void OnThemeChanged(Theme _, Theme newTheme)
+ {
+ UpdateIconPath(newTheme);
+ }
+
+ private static void UpdateIconPath(Theme theme)
+ {
+ if (theme == Theme.Light || theme == Theme.HighContrastWhite)
+ {
+ _icon_path = "Images/unitconverter.light.png";
+ }
+ else
+ {
+ _icon_path = "Images/unitconverter.dark.png";
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _context.API.ThemeChanged -= OnThemeChanged;
+ _disposed = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..657e7ce98
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Community.PowerToys.Run.Plugin.UnitConverter.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy (Enter).
+ ///
+ public static string context_menu_copy {
+ get {
+ return ResourceManager.GetString("context_menu_copy", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy failed.
+ ///
+ public static string copy_failed {
+ get {
+ return ResourceManager.GetString("copy_failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy {0} to clipboard.
+ ///
+ public static string copy_to_clipboard {
+ get {
+ return ResourceManager.GetString("copy_to_clipboard", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Provides unit conversion (e.g. 10 ft in m)..
+ ///
+ public static string plugin_description {
+ get {
+ return ResourceManager.GetString("plugin_description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unit Converter.
+ ///
+ public static string plugin_name {
+ get {
+ return ResourceManager.GetString("plugin_name", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.resx b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.resx
new file mode 100644
index 000000000..22e07ab65
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/Properties/Resources.resx
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Copy (Enter)
+
+
+ Copy failed
+
+
+ Copy {0} to clipboard
+
+
+ Provides unit conversion (e.g. 10 ft in m).
+
+
+ Unit Converter
+
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitConversionResult.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitConversionResult.cs
new file mode 100644
index 000000000..7a3eb7528
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitConversionResult.cs
@@ -0,0 +1,20 @@
+using UnitsNet;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter
+{
+ public class UnitConversionResult
+ {
+ public double ConvertedValue { get; }
+
+ public string UnitName { get; }
+
+ public QuantityType QuantityType { get; }
+
+ public UnitConversionResult(double convertedValue, string unitName, QuantityType quantityType)
+ {
+ ConvertedValue = convertedValue;
+ UnitName = unitName;
+ QuantityType = quantityType;
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs
new file mode 100644
index 000000000..a084205dc
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using UnitsNet;
+
+namespace Community.PowerToys.Run.Plugin.UnitConverter
+{
+ public static class UnitHandler
+ {
+ private static readonly int _roundingFractionalDigits = 4;
+
+ private static readonly QuantityType[] _included = new QuantityType[]
+ {
+ QuantityType.Acceleration,
+ QuantityType.Angle,
+ QuantityType.Area,
+ QuantityType.Duration,
+ QuantityType.Energy,
+ QuantityType.Information,
+ QuantityType.Length,
+ QuantityType.Mass,
+ QuantityType.Power,
+ QuantityType.Pressure,
+ QuantityType.Speed,
+ QuantityType.Temperature,
+ QuantityType.Volume,
+ };
+
+ ///
+ /// Given string representation of unit, converts it to the enum.
+ ///
+ /// Corresponding enum or null.
+ private static Enum GetUnitEnum(string unit, QuantityInfo unitInfo)
+ {
+ UnitInfo first = Array.Find(unitInfo.UnitInfos, info => info.Name.ToLower() == unit.ToLower());
+ if (first != null)
+ {
+ return first.Value;
+ }
+
+ if (UnitParser.Default.TryParse(unit, unitInfo.UnitType, out Enum enum_unit))
+ {
+ return enum_unit;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Given parsed ConvertModel, computes result. (E.g "1 foot in cm").
+ ///
+ /// The converted value as a double.
+ public static double ConvertInput(ConvertModel convertModel, QuantityType quantityType)
+ {
+ QuantityInfo unitInfo = Quantity.GetInfo(quantityType);
+
+ var fromUnit = GetUnitEnum(convertModel.FromUnit, unitInfo);
+ var toUnit = GetUnitEnum(convertModel.ToUnit, unitInfo);
+
+ if (fromUnit != null && toUnit != null)
+ {
+ return UnitsNet.UnitConverter.Convert(convertModel.Value, fromUnit, toUnit);
+ }
+
+ return double.NaN;
+ }
+
+ ///
+ /// Given ConvertModel returns collection of possible results.
+ ///
+ /// The converted value as a double.
+ public static IEnumerable Convert(ConvertModel convertModel)
+ {
+ var results = new List();
+ foreach (QuantityType quantityType in _included)
+ {
+ double convertedValue = UnitHandler.ConvertInput(convertModel, quantityType);
+
+ if (!double.IsNaN(convertedValue))
+ {
+ UnitConversionResult result = new UnitConversionResult(Math.Round(convertedValue, _roundingFractionalDigits), convertModel.ToUnit, quantityType);
+ results.Add(result);
+ }
+ }
+
+ return results;
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/plugin.json b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/plugin.json
new file mode 100644
index 000000000..267dd5687
--- /dev/null
+++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/plugin.json
@@ -0,0 +1,13 @@
+{
+ "ID": "aa0ee9daff654fb7be452c2d77c471b9",
+ "ActionKeyword": "%%",
+ "IsGlobal": false,
+ "Name": "Unit Converter",
+ "Author": "ThiefZero",
+ "Version": "0.0.1",
+ "Language": "csharp",
+ "Website": "https://github.com/ThiefZero",
+ "IcoPathDark": "Images\\unitconverter.dark.png",
+ "IcoPathLight": "Images\\unitconverter.light.png",
+ "ExecuteFileName": "Community.PowerToys.Run.Plugin.UnitConverter.dll"
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/Main.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/Main.cs
index 87d60488c..822531f2c 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/Main.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/Main.cs
@@ -74,7 +74,6 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
UpdateIconPath(Context.API.GetCurrentTheme());
}
- // Todo : Update with theme based IconPath
private void UpdateIconPath(Theme theme)
{
if (theme == Theme.Light || theme == Theme.HighContrastWhite)
diff --git a/src/modules/launcher/PowerLauncher/PowerLauncher.csproj b/src/modules/launcher/PowerLauncher/PowerLauncher.csproj
index 9745aa61d..c80474633 100644
--- a/src/modules/launcher/PowerLauncher/PowerLauncher.csproj
+++ b/src/modules/launcher/PowerLauncher/PowerLauncher.csproj
@@ -113,6 +113,7 @@
NU1701
+