diff --git a/PowerToys.sln b/PowerToys.sln
index ca9bb2e58c..db9b22afda 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -189,6 +189,7 @@ 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
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871} = {03276A39-D4E9-417C-8FFD-200B0EE5E871}
{FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4}
{59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E}
{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}
@@ -263,6 +264,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPickerUI", "src\module
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "colorpicker", "colorpicker", "{1D78B84B-CA39-406C-98F4-71F7EC266CC0}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Uri", "src\modules\launcher\Plugins\Microsoft.Plugin.Uri\Microsoft.Plugin.Uri.csproj", "{03276A39-D4E9-417C-8FFD-200B0EE5E871}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Uri.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Uri.UnitTests\Microsoft.Plugin.Uri.UnitTests.csproj", "{B81FB7B6-D30E-428F-908A-41422EFC1172}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -521,6 +526,14 @@ Global
{BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x64.Build.0 = Debug|x64
{BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.ActiveCfg = Release|x64
{BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.Build.0 = Release|x64
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.ActiveCfg = Debug|x64
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.Build.0 = Debug|x64
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.ActiveCfg = Release|x64
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.Build.0 = Release|x64
+ {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.ActiveCfg = Debug|x64
+ {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.Build.0 = Debug|x64
+ {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.ActiveCfg = Release|x64
+ {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -595,6 +608,8 @@ Global
{655C9AF2-18D3-4DA6-80E4-85504A7722BA} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0}
{BA58206B-1493-4C75-BFEA-A85768A1E156} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0}
{1D78B84B-CA39-406C-98F4-71F7EC266CC0} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {03276A39-D4E9-417C-8FFD-200B0EE5E871} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
+ {B81FB7B6-D30E-428F-908A-41422EFC1172} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 60ae35fb10..8dcbca9bc9 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -275,6 +275,11 @@
+
+
+
+
+
@@ -1013,6 +1018,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/Microsoft.Plugin.Uri.UnitTests.csproj b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/Microsoft.Plugin.Uri.UnitTests.csproj
new file mode 100644
index 0000000000..288c310638
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/Microsoft.Plugin.Uri.UnitTests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.1
+
+ false
+ x64
+ Microsoft.Plugin.Uri.UnitTests
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs
new file mode 100644
index 0000000000..eea81a4c39
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri.UnitTests/UriHelper/ExtendedUriParserTests.cs
@@ -0,0 +1,53 @@
+using Microsoft.Plugin.Uri.UriHelper;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using NUnit.Framework;
+
+namespace Microsoft.Plugin.Uri.UnitTests.UriHelper
+{
+ [TestFixture]
+ public class ExtendedUriParserTests
+ {
+ [TestCase("google.com", true, "http://google.com/")]
+ [TestCase("localhost", true, "http://localhost/")]
+ [TestCase("127.0.0.1", true, "http://127.0.0.1/")]
+ [TestCase("127.0.0.1:80", true, "http://127.0.0.1/")]
+ [TestCase("127", true, "http://0.0.0.127/")]
+ [TestCase("", false, null)]
+ [TestCase("https://google.com", true, "https://google.com/")]
+ [TestCase("ftps://google.com", true, "ftps://google.com/")]
+ [TestCase(null, false, null)]
+ [TestCase("bing.com/search?q=gmx", true, "http://bing.com/search?q=gmx")]
+ [TestCase("h", true, "http://h/")]
+ [TestCase("ht", true, "http://ht/")]
+ [TestCase("htt", true, "http://htt/")]
+ [TestCase("http", true, "http://http/")]
+ [TestCase("http:", false, null)]
+ [TestCase("http:/", false, null)]
+ [TestCase("http://", false, null)]
+ [TestCase("http://t", true, "http://t/")]
+ [TestCase("http://te", true, "http://te/")]
+ [TestCase("http://tes", true, "http://tes/")]
+ [TestCase("http://test", true, "http://test/")]
+ [TestCase("http://test.", false, null)]
+ [TestCase("http://test.c", true, "http://test.c/")]
+ [TestCase("http://test.co", true, "http://test.co/")]
+ [TestCase("http://test.com", true, "http://test.com/")]
+ [TestCase("http:3", true,"http://http:3/")]
+ [TestCase("[::]", true, "http://[::]")]
+ [TestCase("[2001:0DB8::1]", true, "http://[2001:0DB8::1]/")]
+ [TestCase("[2001:0DB8::1]:80",true, "http://[2001:0DB8::1]/")]
+ public void TryParse_CanParseHostName(string query, bool expectedSuccess, string expectedResult)
+ {
+ // Arrange
+ var parser = new ExtendedUriParser();
+
+ // Act
+ var success = parser.TryParse(query, out var result);
+
+ // Assert
+ Assert.AreEqual(expectedResult, result?.ToString());
+ Assert.AreEqual(expectedSuccess, success);
+ }
+ }
+}
+
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png
new file mode 100644
index 0000000000..6c34ed1582
Binary files /dev/null and b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.dark.png differ
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png
new file mode 100644
index 0000000000..bea54d005c
Binary files /dev/null and b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Images/uri.light.png differ
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IRegistryWrapper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IRegistryWrapper.cs
new file mode 100644
index 0000000000..4b1682df16
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IRegistryWrapper.cs
@@ -0,0 +1,11 @@
+// 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 Microsoft.Plugin.Uri.Interfaces
+{
+ public interface IRegistryWrapper
+ {
+ string GetRegistryValue(string registryLocation, string valueName);
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs
new file mode 100644
index 0000000000..aa0154bc35
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUriParser.cs
@@ -0,0 +1,11 @@
+// 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 Microsoft.Plugin.Uri.Interfaces
+{
+ public interface IUriParser
+ {
+ bool TryParse(string input, out System.Uri result);
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUrlResolver.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUrlResolver.cs
new file mode 100644
index 0000000000..717b67bf68
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Interfaces/IUrlResolver.cs
@@ -0,0 +1,11 @@
+// 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 Microsoft.Plugin.Uri.Interfaces
+{
+ public interface IUrlResolver
+ {
+ bool IsValidHost(System.Uri uri);
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/de.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/de.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/de.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/en.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/en.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/en.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/ja.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/ja.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/ja.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/pl.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/pl.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/pl.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/tr.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/tr.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/tr.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-cn.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-cn.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-cn.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-tw.xaml b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-tw.xaml
new file mode 100644
index 0000000000..2f2a319d1e
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Languages/zh-tw.xaml
@@ -0,0 +1,5 @@
+
+ Open in browser
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs
new file mode 100644
index 0000000000..0479b566d3
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Main.cs
@@ -0,0 +1,179 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using Microsoft.Plugin.Uri.UriHelper;
+using Wox.Infrastructure.Logger;
+using Wox.Infrastructure.Storage;
+using Wox.Plugin;
+
+namespace Microsoft.Plugin.Uri
+{
+ public class Main : IPlugin, IPluginI18n, IContextMenu, ISavable, IDisposable
+ {
+ private readonly ExtendedUriParser _uriParser;
+ private readonly UriResolver _uriResolver;
+ private readonly PluginJsonStorage _storage;
+ private bool _disposed;
+ private UriSettings _uriSettings;
+ private RegisteryWrapper _registeryWrapper;
+
+ public Main()
+ {
+ _storage = new PluginJsonStorage();
+ _uriSettings = _storage.Load();
+ _uriParser = new ExtendedUriParser();
+ _uriResolver = new UriResolver();
+ _registeryWrapper = new RegisteryWrapper();
+ }
+
+ public string BrowserIconPath { get; set; }
+
+ public string DefaultIconPath { get; set; }
+
+ public PluginInitContext Context { get; protected set; }
+
+ public List LoadContextMenus(Result selectedResult)
+ {
+ return new List(0);
+ }
+
+ public List Query(Query query)
+ {
+ var results = new List();
+
+ if (!string.IsNullOrEmpty(query?.Search)
+ && _uriParser.TryParse(query.Search, out var uriResult)
+ && _uriResolver.IsValidHost(uriResult))
+ {
+ var uriResultString = uriResult.ToString();
+
+ results.Add(new Result
+ {
+ Title = uriResultString,
+ SubTitle = Context.API.GetTranslation("Microsoft_plugin_uri_website"),
+ IcoPath = _uriSettings.ShowBrowserIcon
+ ? BrowserIconPath
+ : DefaultIconPath,
+ Action = action =>
+ {
+ Process.Start(new ProcessStartInfo(uriResultString)
+ {
+ UseShellExecute = true,
+ });
+ return true;
+ },
+ });
+ }
+
+ return results;
+ }
+
+ public void Init(PluginInitContext context)
+ {
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Context.API.ThemeChanged += OnThemeChanged;
+ UpdateIconPath(Context.API.GetCurrentTheme());
+ UpdateBrowserIconPath(Context.API.GetCurrentTheme());
+ }
+
+ public string GetTranslatedPluginTitle()
+ {
+ return "Url Handler";
+ }
+
+ public string GetTranslatedPluginDescription()
+ {
+ return "Handles urls";
+ }
+
+ public void Save()
+ {
+ _storage.Save();
+ }
+
+ private void OnThemeChanged(Theme oldtheme, Theme newTheme)
+ {
+ UpdateIconPath(newTheme);
+ UpdateBrowserIconPath(newTheme);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to keep the process alive but will log the exception")]
+ private void UpdateBrowserIconPath(Theme newTheme)
+ {
+ try
+ {
+ var progId = _registeryWrapper.GetRegistryValue("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", "ProgId");
+ var programLocation =
+
+ // Resolve App Icon (UWP)
+ _registeryWrapper.GetRegistryValue("HKEY_CLASSES_ROOT\\" + progId + "\\Application", "ApplicationIcon")
+
+ // Resolves default file association icon (UWP + Normal)
+ ?? _registeryWrapper.GetRegistryValue("HKEY_CLASSES_ROOT\\" + progId + "\\DefaultIcon", null);
+
+ // "Handles 'Indirect Strings' (UWP programs)"
+ if (programLocation.StartsWith("@", StringComparison.Ordinal))
+ {
+ var directProgramLocationStringBuilder = new StringBuilder(128);
+ if (NativeMethods.SHLoadIndirectString(programLocation, directProgramLocationStringBuilder, (uint)directProgramLocationStringBuilder.Capacity, IntPtr.Zero) ==
+ NativeMethods.Hresult.Ok)
+ {
+ // Check if there's a postfix with contract-white/contrast-black icon is available and use that instead
+ var directProgramLocation = directProgramLocationStringBuilder.ToString();
+ var themeIcon = newTheme == Theme.Light || newTheme == Theme.HighContrastWhite ? "contrast-white" : "contrast-black";
+ var extension = Path.GetExtension(directProgramLocation);
+ var themedProgLocation = $"{directProgramLocation.Substring(0, directProgramLocation.Length - extension.Length)}_{themeIcon}{extension}";
+ BrowserIconPath = File.Exists(themedProgLocation)
+ ? themedProgLocation
+ : directProgramLocation;
+ }
+ }
+ else
+ {
+ var indexOfComma = programLocation.IndexOf(',', StringComparison.Ordinal);
+ BrowserIconPath = indexOfComma > 0
+ ? programLocation.Substring(0, indexOfComma)
+ : programLocation;
+ }
+ }
+ catch (Exception e)
+ {
+ BrowserIconPath = DefaultIconPath;
+ Log.Exception("Exception when retreiving icon", e);
+ }
+ }
+
+ private void UpdateIconPath(Theme theme)
+ {
+ if (theme == Theme.Light || theme == Theme.HighContrastWhite)
+ {
+ DefaultIconPath = "Images/uri.light.png";
+ }
+ else
+ {
+ DefaultIconPath = "Images/uri.dark.png";
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed && disposing)
+ {
+ Context.API.ThemeChanged -= OnThemeChanged;
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Microsoft.Plugin.Uri.csproj b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Microsoft.Plugin.Uri.csproj
new file mode 100644
index 0000000000..966f89b04c
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/Microsoft.Plugin.Uri.csproj
@@ -0,0 +1,133 @@
+
+
+
+ netcoreapp3.1
+ {03276a39-d4e9-417c-8ffd-200b0ee5e871}
+ Properties
+ Microsoft.Plugin.Uri
+ Microsoft.Plugin.Uri
+ true
+ false
+ false
+ x64
+
+
+
+ true
+ ..\..\..\..\..\x64\Debug\modules\launcher\Plugins\Microsoft.Plugin.Uri\
+ DEBUG;TRACE
+ full
+ x64
+ 7.3
+ prompt
+ MinimumRecommendedRules.ruleset
+ 4
+ false
+ true
+
+
+
+ ..\..\..\..\..\x64\Release\modules\launcher\Plugins\Microsoft.Plugin.Uri\
+ TRACE
+ true
+ pdbonly
+ x64
+ 7.3
+ prompt
+ MinimumRecommendedRules.ruleset
+ 4
+ true
+
+
+
+
+ GlobalSuppressions.cs
+
+
+ StyleCop.json
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+ PreserveNewest
+ Designer
+ MSBuild:Compile
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/NativeMethods.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/NativeMethods.cs
new file mode 100644
index 0000000000..c64881736b
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/NativeMethods.cs
@@ -0,0 +1,21 @@
+// 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.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Microsoft.Plugin.Uri
+{
+ internal static class NativeMethods
+ {
+ internal enum Hresult : uint
+ {
+ Ok = 0x0000,
+ }
+
+ [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
+ internal static extern Hresult SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, IntPtr ppvReserved);
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/RegisteryWrapper.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/RegisteryWrapper.cs
new file mode 100644
index 0000000000..86993799ae
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/RegisteryWrapper.cs
@@ -0,0 +1,17 @@
+// 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.
+
+using Microsoft.Plugin.Uri.Interfaces;
+using Microsoft.Win32;
+
+namespace Microsoft.Plugin.Uri
+{
+ public class RegisteryWrapper : IRegistryWrapper
+ {
+ public string GetRegistryValue(string registryLocation, string valueName)
+ {
+ return Registry.GetValue(registryLocation, valueName, null) as string;
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs
new file mode 100644
index 0000000000..0d6e202e82
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/ExtendedUriParser.cs
@@ -0,0 +1,43 @@
+// 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.
+
+using System;
+using Microsoft.Plugin.Uri.Interfaces;
+
+namespace Microsoft.Plugin.Uri.UriHelper
+{
+ public class ExtendedUriParser : IUriParser
+ {
+ public bool TryParse(string input, out System.Uri result)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ result = default;
+ return false;
+ }
+
+ // Handle common cases UriBuilder does not handle
+ if (input.EndsWith(":", StringComparison.Ordinal)
+ || input.EndsWith(".", StringComparison.Ordinal)
+ || input.EndsWith(":/", StringComparison.Ordinal))
+ {
+ result = default;
+ return false;
+ }
+
+ try
+ {
+ var urlBuilder = new UriBuilder(input);
+
+ result = urlBuilder.Uri;
+ return true;
+ }
+ catch (System.UriFormatException)
+ {
+ result = default;
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/UriResolver.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/UriResolver.cs
new file mode 100644
index 0000000000..b22c693395
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriHelper/UriResolver.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using Microsoft.Plugin.Uri.Interfaces;
+
+namespace Microsoft.Plugin.Uri.UriHelper
+{
+ public class UriResolver : IUrlResolver
+ {
+ public bool IsValidHost(System.Uri uri)
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriSettings.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriSettings.cs
new file mode 100644
index 0000000000..7f91676425
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/UriSettings.cs
@@ -0,0 +1,11 @@
+// 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 Microsoft.Plugin.Uri
+{
+ public class UriSettings
+ {
+ public bool ShowBrowserIcon { get; set; } = true;
+ }
+}
diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/plugin.json b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/plugin.json
new file mode 100644
index 0000000000..38347f46b0
--- /dev/null
+++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Uri/plugin.json
@@ -0,0 +1,12 @@
+{
+ "ID": "03276A39D4E9417C8FFD200B0EE5E871",
+ "ActionKeyword": "*",
+ "Name": "Windows Uri Handler",
+ "Description": "Handles urls",
+ "Author": "Microsoft",
+ "Version": "1.0.0",
+ "Language": "csharp",
+ "Website": "http://aka.ms/PowerToys",
+ "ExecuteFileName": "Microsoft.Plugin.Uri.dll",
+ "IcoPath": "Images\\uri.dark.png"
+}