From 06afe099734d3be7ed723666d829a3ce07354be0 Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Sat, 29 Nov 2025 13:07:19 -0600 Subject: [PATCH] CmdPal: New Remote Desktop built-in extension (#43090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces a new built-in extension for Remote Desktop users. It allows you to view past RDP connections, save predefined connections, and connect to any of them. Or start a new RDP connection. https://github.com/user-attachments/assets/6a5041a6-5741-4df0-a305-da7166f962e1 ### GitHub issue maintenance stuff Closes #38305 --------- Co-authored-by: Niels Laute Co-authored-by: Jiří Polášek --- .github/actions/spell-check/expect.txt | 8 +- Directory.Packages.props | 2 +- PowerToys.sln | 22 +++ .../Commands/MainListPage.cs | 1 + .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 2 + .../Microsoft.CmdPal.UI.csproj | 1 + .../FallbackRemoteDesktopItemTests.cs | 125 ++++++++++++++ ....CmdPal.Ext.RemoteDesktop.UnitTests.csproj | 24 +++ .../MockRDPConnectionsManager.cs | 23 +++ .../MockSettingsManager.cs | 23 +++ .../RDPConnectionsManagerTests.cs | 52 ++++++ .../RemoteDesktopCommandProviderTests.cs | 101 ++++++++++++ .../Assets/RemoteDesktop.png | Bin 0 -> 3504 bytes .../Assets/RemoteDesktop.svg | 21 +++ .../Commands/ConnectionListItem.cs | 35 ++++ .../Commands/FallbackRemoteDesktopItem.cs | 74 +++++++++ .../Commands/OpenRemoteDesktopCommand.cs | 82 ++++++++++ .../Helper/ConnectionHelpers.cs | 30 ++++ .../Helper/IRDPConnectionManager.cs | 13 ++ .../Helper/RDPConnectionsManager.cs | 89 ++++++++++ .../Icons.cs | 12 ++ .../Microsoft.CmdPal.Ext.RemoteDesktop.csproj | 44 +++++ .../Pages/RemoteDesktopListPage.cs | 27 ++++ .../Properties/AssemblyInfo.cs | 7 + .../Properties/Resources.Designer.cs | 153 ++++++++++++++++++ .../Properties/Resources.resx | 150 +++++++++++++++++ .../RemoteDesktopCommandProvider.cs | 45 ++++++ .../Settings/ISettingsInterface.cs | 15 ++ .../Settings/SettingsManager.cs | 53 ++++++ 29 files changed, 1232 insertions(+), 2 deletions(-) create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 9f18bbb300..c77154b466 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -727,7 +727,6 @@ HWNDPARENT HWNDPREV hyjiacan IAI -icf ICONERROR ICONLOCATION IDCANCEL @@ -800,6 +799,7 @@ invokecommand ipcmanager IPREVIEW ipreviewhandlervisualssetfont +irdp IPTC irow irprops @@ -1057,6 +1057,7 @@ msrc msstore msvcp MT +mstsc MTND MULTIPLEUSE multizone @@ -1206,8 +1207,11 @@ OOBEUI openas opencode OPENFILENAME +openrdp opensource openxmlformats +ollama +onnx OPTIMIZEFORINVOKE ORPHANEDDIALOGTITLE ORSCANS @@ -1420,6 +1424,8 @@ RAWPATH rbhid rclsid RCZOOMIT +remotedesktop +rdp RDW READMODE READOBJECTS diff --git a/Directory.Packages.props b/Directory.Packages.props index 75b2399c8a..6744b991aa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,7 +69,7 @@ This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail. --> - + diff --git a/PowerToys.sln b/PowerToys.sln index e34779c5bb..4e42cc9e30 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -834,6 +834,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj", "{2B3FB837-23DE-629F-82C6-42304E7083C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj", "{DB34808A-FF91-D06E-A426-AFB5A8BD583B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -3036,6 +3040,22 @@ Global {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64 {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64 {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.Build.0 = Debug|ARM64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.ActiveCfg = Debug|x64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.Build.0 = Debug|x64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.ActiveCfg = Release|ARM64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.Build.0 = Release|ARM64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.ActiveCfg = Release|x64 + {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.Build.0 = Release|x64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.Build.0 = Debug|ARM64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.ActiveCfg = Debug|x64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.Build.0 = Debug|x64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.ActiveCfg = Release|ARM64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.Build.0 = Release|ARM64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.ActiveCfg = Release|x64 + {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3367,6 +3387,8 @@ Global {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482} {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {2B3FB837-23DE-629F-82C6-42304E7083C9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} + {DB34808A-FF91-D06E-A426-AFB5A8BD583B} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 996475d559..b13a72d276 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -36,6 +36,7 @@ public partial class MainListPage : DynamicListPage, "com.microsoft.cmdpal.builtin.websearch", "com.microsoft.cmdpal.builtin.windowssettings", "com.microsoft.cmdpal.builtin.datetime", + "com.microsoft.cmdpal.builtin.remotedesktop", ]; private readonly IServiceProvider _serviceProvider; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 917716be19..f91b9e304a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Calc; using Microsoft.CmdPal.Ext.ClipboardHistory; using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CmdPal.Ext.Registry; +using Microsoft.CmdPal.Ext.RemoteDesktop; using Microsoft.CmdPal.Ext.Shell; using Microsoft.CmdPal.Ext.System; using Microsoft.CmdPal.Ext.TimeDate; @@ -151,6 +152,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Models services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index bd7402e4fd..f1702c302c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -118,6 +118,7 @@ + diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs new file mode 100644 index 0000000000..837246b731 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs @@ -0,0 +1,125 @@ +// 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.Globalization; +using System.Reflection; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class FallbackRemoteDesktopItemTests +{ + private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + + [TestMethod] + public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName() + { + var connectionName = "my-rdp-server"; + + // Arrange + var setup = CreateFallback(connectionName); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery("my-rdp-server"); + + // Assert + Assert.AreEqual(connectionName, fallback.Title); + var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName); + Assert.AreEqual(expectedSubtitle, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name); + Assert.AreEqual(connectionName, GetCommandHost(command)); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery() + { + // Arrange + var setup = CreateFallback(); + var fallback = setup.Fallback; + const string hostname = "test.corp"; + + // Act + fallback.UpdateQuery(hostname); + + // Assert + var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname); + Assert.AreEqual(expectedTitle, fallback.Title); + Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name); + Assert.AreEqual(hostname, GetCommandHost(command)); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand() + { + // Arrange + var setup = CreateFallback("rdp-server-two"); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery(" "); + + // Assert + Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title); + Assert.AreEqual(string.Empty, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_open, command.Name); + Assert.AreEqual(string.Empty, GetCommandHost(command)); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand() + { + // Arrange + var setup = CreateFallback("rdp-server-three"); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery("not a valid host"); + + // Assert + Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title); + Assert.AreEqual(string.Empty, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_open, command.Name); + Assert.AreEqual(string.Empty, GetCommandHost(command)); + } + + private static string GetCommandHost(OpenRemoteDesktopCommand command) + { + var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance); + if (field is null) + { + return string.Empty; + } + + return field.GetValue(command) as string ?? string.Empty; + } + + private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionManager Manager) CreateFallback(params string[] connectionNames) + { + var settingsManager = new MockSettingsManager(connectionNames); + var connectionsManager = new MockRDPConnectionsManager(settingsManager); + + var fallback = new FallbackRemoteDesktopItem(connectionsManager); + + return (fallback, connectionsManager); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj new file mode 100644 index 0000000000..0b998ec4ad --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + + + false + true + Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\ + false + false + enable + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs new file mode 100644 index 0000000000..36c3151c6f --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs @@ -0,0 +1,23 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +internal sealed class MockRDPConnectionsManager : IRdpConnectionManager +{ + private readonly List _connections = new(); + + public IReadOnlyCollection Connections => _connections.AsReadOnly(); + + public MockRDPConnectionsManager(ISettingsInterface settingsManager) + { + _connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult)); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs new file mode 100644 index 0000000000..1a81dcc7ee --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs @@ -0,0 +1,23 @@ +// 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.Collections.Generic; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +internal sealed class MockSettingsManager : ISettingsInterface +{ + private readonly List _connections; + + public IReadOnlyCollection PredefinedConnections => _connections; + + public ToolkitSettings Settings { get; } = new(); + + public MockSettingsManager(params string[] predefinedConnections) + { + _connections = new(predefinedConnections); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs new file mode 100644 index 0000000000..dabea49a63 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs @@ -0,0 +1,52 @@ +// 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.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class RDPConnectionsManagerTests +{ + [TestMethod] + public void Constructor_AddsOpenCommandItem() + { + // Act + var manager = new RDPConnectionsManager(new MockSettingsManager(["test.local"])); + + // Assert + Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName))); + } + + [TestMethod] + public void FindConnection_ReturnsExactMatch() + { + // Arrange + var connectionName = "rdp-test"; + var connection = new ConnectionListItem(connectionName); + + // Act + var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection }); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(connectionName, result.ConnectionName); + } + + [TestMethod] + public void FindConnection_ReturnsNullForWhitespaceQuery() + { + // Arrange + var connection = new ConnectionListItem("rdp-test"); + + // Act + var result = ConnectionHelpers.FindConnection(" ", new[] { connection }); + + // Assert + Assert.IsNull(result); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs new file mode 100644 index 0000000000..54698997ee --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs @@ -0,0 +1,101 @@ +// 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.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Pages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class RemoteDesktopCommandProviderTests +{ + [TestMethod] + public void ProviderHasCorrectId() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id); + } + + [TestMethod] + public void ProviderHasDisplayName() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.IsNotNull(provider.DisplayName); + Assert.IsTrue(provider.DisplayName.Length > 0); + } + + [TestMethod] + public void ProviderHasIcon() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.IsNotNull(provider.Icon); + } + + [TestMethod] + public void TopLevelCommandsNotEmpty() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void FallbackCommandsNotEmpty() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.FallbackCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void TopLevelCommandsContainListPageCommand() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.AreEqual(1, commands.Length); + Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage)); + } + + [TestMethod] + public void FallbackCommandsContainFallbackItem() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.FallbackCommands(); + + // Assert + Assert.AreEqual(1, commands.Length); + Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem)); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png new file mode 100644 index 0000000000000000000000000000000000000000..52d97dbfe9d119f992174ae8eeabdf5a7a11fc37 GIT binary patch literal 3504 zcmV;h4NvlkP)@~0drDELIAGL9O(c600d`2O+f$vv5yPI9Qni+*kZB2lh6qQz>jW9FDGN3YZZhLj#*4}IX=L~R2nvnEt_&NV&|9h|R zTi;sy@&j~Ar*ulEbV{dm%KvLo#Ty&r-g=Hl5&V7@xNIigalv43VG@Kh{nSz=@1Vb`1lq5(1gSZdMLLpO; zilB+@;a@3z1n0!i44rEcvgV(8XqNzQ00LNZluF@TrT8M@Ggn{b9<tX+aX@o(2j@l8`BeNdTqX z;GSs+GLpx&Bvkx{x}?Jm3T&_fVg=b)dj$9vToTOgr7)10I-Uz45Q$FKiETjS2Kf$B zyU4tpLM4Qp0+O?=LqL)+f+V>;0-|(^UCVP)uZ@Ugp6t^yy4-G8B*!v=|AZGJ|az z7n}ss(8kH7X}Ag!qfE-GV5|g$Je({Y@qG~nKpaN-UOt(EAV&EFEmbH;KQHuX83H*7 zwn_^HAatSwm}n@!r}LO}M3O?T&g89(piCWJH>qs}5>AORuc0DGPa_x$jqVrUfE&Xb@pgcA5Xu~ELJ2+w*=@?O1wuxbHtX}5CW zf{;VFPRZPx-$Ra=n=T{Vb2;I2Gxh(=Ny2N#l_Tat6=3zG;+QW%P)IJ$1tAWjQQTWl z1Sg|N1U2V})FF7?anAycRr3jRy5WGU(57fUNb5&B}O4zlK`69 zic8g`Ec55e9*rL_s^hBO@ODH?nXeovK@D!@t@ugENyuOOQV-Cju>bIpV^86{3Gm8o z8eWRCEh^N}qGMhYqy$J}wyLrOb)|{pC$FhuPM7veClzvx3ZPpU?Z67Q3HfhxQJdfU zXKNB#%>zQ%@pP1BB@{4p;;loE(;@kxI_>oDA!6OnM{#0dJ>=fX^Lx&HqoRI*M=f1~m$CZiBm5^q8ZW9IYQ(PGj!uUJp z*71dIE#t$T$Dve!VJg%5MEji|k}A;ge9U2YJjOqg=aC4uSL0(RGVnvo zR+wWsg312ZPpDSx-@m`#UeB-?3I@B`T-t^XPFK1hJ01lP0BZS6Q`+7#e-`!DF|Ui_ zbDzDvdj5Uv2wvZ@)jeCbVwF2uxF`0Lu8ikT6u=J~@W%U;q$m}mdS;iOFsIM941pN8 zge>-h9I`Nov9EXyuB4gBi3M(+Q;WF)tgzouc|98isI}(@ zmn}~!$pCs#Mzasv%hn;l7~|V2i%XLANT^1tM-T`>6$#z|qcI&MZGOj29LJV|=ZB5#9=`c#t$Q0w@GqH&$SpCVkktG z#Y|E>>+jMTQDCw}(ku7%;bB8UIgqgT%~3{62!e=8?CV94M3! z?Ct66>ua$q?8jmG6(;*289WZ*VO>akyt&~+yYS_QX^BcppyD#u{Ov3E;gLrlb7U|H zd-sf{`sTR)hOb7u|M9{5ICW|=g(y)PT_D1j_{>btE0skp6rcrggds1<$g7XCFT;G}82ZpV3BEck6(UdE}($pn-r*5x+g z)T5ojC9Nl0hX9Zz9AS3^2H|PM`sp{qk52U=@t?Yi9mw3^LtJOFwG*B`8xG$|O8C>B zH}TG2-v+M>28WknaA;X1Vf)rsA_*}IxO0Uxr|?ErtJR~ezYUTE7>QAGKFNpt90rkg ziebS-eNt1ASVX`SSz`srYYTq$H`hcH-~HP`L@ef?f#G2c3=IboPGQHkEshKdKnc=| z(J_;Y7cXvBfU)T$dokPDiEWTbl3{xr5C%yF6qI3(=`V^XU>2iCol;1S5|olAs0nzgOZceTg{g*$ZMr|&PcidJlmS06 z=HzVG^$dRL&=9g=O$W-u9ARK!2ulYBJqZT<&K=v+=n&p(G#Z%teEQb`UVG*0$9_ps zs;<-A07rp#9wdZpTP#nmy9M7Kyw)#$dBc;!c39XPnV6U`#&3<_t!FI(L}4#b#2JCY zdcyWfZZCP?TYK^LpZB@v_uh9uuD$L$SG8WRt6HtT|L!{%f5YDBMw^91pv&P5A}<8r z&q(gheIFVE7I`qvY)^v6WWkT)_1Cru@Po#yLq6uiQj{S2bUnJ9;_H4 ztDQR{_$5ZpOO|@ss{k~OPSj)JD$Nj=*tOR6+-C+l(3mYSZfU}|dGjUtswb1wu5czHg6 zm}n@KvrxRYW&G3>PE4M`UDwWYVCKx3jrj`};Fepz?&MogcqVTY$fc0ogVa^E>OEhp z#beXjZ>`x_e79`^=H9ZdVtQNaJC4B%G4Rd!uB~4xB_=E>AC7;DPp2xl^_t6EqAHjB zap|RVk@oq7mvufzuz>Ah6=V+&zD@?XOLiW(BV^OMwQD!FvZ?K1(3UG6er|)&;Y#hN zREoUrlvb1o2|H;eWj9ro*^*aEa~vp__Bo8~K0K|t?F#&G^}>~^R9aXvpTJ5bf5>lx z()lSQ{E%yiP>qJq1@(Hvm}802lSK7R_uqHlGc#eiAOikZWn^SzVYNmZRf(SP;7jf^ zIL{A63E{5Y|K+obCaWH|N_D{Pri&h>M(OGG>+d~qwmvT^0=Vqny?dqkUSz-JJzz=j z^Ch|CsX`6_0000 + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs new file mode 100644 index 0000000000..888a1d2f71 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs @@ -0,0 +1,35 @@ +// 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.Globalization; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class ConnectionListItem : ListItem +{ + public ConnectionListItem(string connectionName) + { + ConnectionName = connectionName; + + if (string.IsNullOrEmpty(connectionName)) + { + Title = Resources.remotedesktop_open_rdp; + Subtitle = Resources.remotedesktop_subtitle; + } + else + { + Title = connectionName; + CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName); + } + + Icon = Icons.RDPIcon; + Command = new OpenRemoteDesktopCommand(connectionName); + } + + public string ConnectionName { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs new file mode 100644 index 0000000000..415746670f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs @@ -0,0 +1,74 @@ +// 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.Globalization; +using System.Linq; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem +{ + private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback"; + + private static readonly UriHostNameType[] ValidUriHostNameTypes = [ + UriHostNameType.IPv6, + UriHostNameType.IPv4, + UriHostNameType.Dns + ]; + + private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + private readonly IRdpConnectionManager _rdpConnectionsManager; + + public FallbackRemoteDesktopItem(IRdpConnectionManager rdpConnectionsManager) + : base(new OpenRemoteDesktopCommand(string.Empty), Resources.remotedesktop_title) + { + _rdpConnectionsManager = rdpConnectionsManager; + + Title = string.Empty; + Subtitle = string.Empty; + Icon = Icons.RDPIcon; + } + + public override void UpdateQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + Title = string.Empty; + Subtitle = string.Empty; + Command = new OpenRemoteDesktopCommand(string.Empty); + return; + } + + var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName)); + + var queryConnection = ConnectionHelpers.FindConnection(query, connections); + + if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName)) + { + var connectionName = queryConnection.ConnectionName; + + Command = new OpenRemoteDesktopCommand(connectionName); + Title = connectionName; + Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName); + } + else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query))) + { + var connectionName = query.Trim(); + Command = new OpenRemoteDesktopCommand(connectionName); + Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName); + Subtitle = Resources.remotedesktop_title; + } + else + { + Title = string.Empty; + Subtitle = string.Empty; + Command = new OpenRemoteDesktopCommand(string.Empty); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs new file mode 100644 index 0000000000..679b015a1e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs @@ -0,0 +1,82 @@ +// 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.Diagnostics; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand +{ + private static readonly CompositeFormat ProcessErrorFormat = + CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error); + + private static readonly CompositeFormat InvalidHostnameFormat = + CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname); + + public string Name { get; } + + public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp"; + + public IIconInfo Icon => Icons.RDPIcon; + + private readonly string _rdpHost; + + public OpenRemoteDesktopCommand(string rdpHost) + { + _rdpHost = rdpHost; + + Name = string.IsNullOrWhiteSpace(_rdpHost) ? + Resources.remotedesktop_command_open : + Resources.remotedesktop_command_connect; + } + + public ICommandResult Invoke(object sender) + { + using var process = new Process(); + process.StartInfo.UseShellExecute = false; + process.StartInfo.WorkingDirectory = Environment.SpecialFolder.MyDocuments.ToString(); + process.StartInfo.FileName = "mstsc"; + + if (!string.IsNullOrWhiteSpace(_rdpHost)) + { + // validate that _rdpHost is a proper hostname or IP address + if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = string.Format( + System.Globalization.CultureInfo.CurrentCulture, + InvalidHostnameFormat, + _rdpHost), + Result = CommandResult.KeepOpen(), + }); + } + + process.StartInfo.Arguments = $"/v:{_rdpHost}"; + } + + try + { + process.Start(); + + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = string.Format( + System.Globalization.CultureInfo.CurrentCulture, + ProcessErrorFormat, + ex.Message), + Result = CommandResult.KeepOpen(), + }); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs new file mode 100644 index 0000000000..5fac986169 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs @@ -0,0 +1,30 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal static class ConnectionHelpers +{ + public static ConnectionListItem MapToResult(string item) => new(item); + + public static ConnectionListItem? FindConnection(string query, IEnumerable connections) + { + if (string.IsNullOrWhiteSpace(query)) + { + return null; + } + + var matchedConnection = ListHelpers.FilterList( + connections, + query, + (s, i) => ListHelpers.ScoreListItem(s, i)) + .FirstOrDefault(); + return matchedConnection; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs new file mode 100644 index 0000000000..4d04126dfb --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs @@ -0,0 +1,13 @@ +// 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.Collections.Generic; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal interface IRdpConnectionManager +{ + IReadOnlyCollection Connections { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs new file mode 100644 index 0000000000..b9b7d62e1a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs @@ -0,0 +1,89 @@ +// 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.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal class RDPConnectionsManager : IRdpConnectionManager +{ + private readonly ISettingsInterface _settingsManager; + private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty); + + private ReadOnlyCollection _connections = new(Array.Empty()); + + private const int MinutesToCache = 1; + private DateTime? _connectionsLastLoaded; + + public RDPConnectionsManager(ISettingsInterface settingsManager) + { + _settingsManager = settingsManager; + _settingsManager.Settings.SettingsChanged += (s, e) => + { + _connectionsLastLoaded = null; + }; + } + + public IReadOnlyCollection Connections + { + get + { + if (!_connectionsLastLoaded.HasValue || + (DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache) + { + var registryConnections = GetRdpConnectionsFromRegistry(); + var predefinedConnections = GetPredefinedConnectionsFromSettings(); + _connectionsLastLoaded = DateTime.Now; + + var newConnections = new List(registryConnections.Count + predefinedConnections.Count + 1); + newConnections.AddRange(registryConnections); + newConnections.AddRange(predefinedConnections); + newConnections.Insert(0, _openRdpCommandListItem); + + Interlocked.Exchange(ref _connections, new ReadOnlyCollection(newConnections)); + } + + return _connections; + } + } + + private List GetRdpConnectionsFromRegistry() + { + using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default"); + + var validConnections = new List(); + + if (key is not null) + { + validConnections = key.GetValueNames() + .Select(name => key.GetValue(name)) + .OfType() // Keep only string values + .Select(v => v.Trim()) // Normalize + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Distinct() // Remove dupes if any + .Select(ConnectionHelpers.MapToResult) + .ToList(); + } + + return validConnections; + } + + private List GetPredefinedConnectionsFromSettings() + { + var validConnections = _settingsManager.PredefinedConnections + .Select(s => s.Trim()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(ConnectionHelpers.MapToResult) + .ToList(); + + return validConnections; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs new file mode 100644 index 0000000000..eec9e48e24 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs @@ -0,0 +1,12 @@ +// 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.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop; + +internal static class Icons +{ + internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg"); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj new file mode 100644 index 0000000000..2a561b9b9e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj @@ -0,0 +1,44 @@ + + + + + + Microsoft.CmdPal.Ext.RemoteDesktop + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.RemoteDesktop.pri + enable + + + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs new file mode 100644 index 0000000000..42a0165277 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs @@ -0,0 +1,27 @@ +// 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.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages; + +internal sealed partial class RemoteDesktopListPage : ListPage +{ + private readonly IRdpConnectionManager _rdpConnectionManager; + + public RemoteDesktopListPage(IRdpConnectionManager rdpConnectionManager) + { + Icon = Icons.RDPIcon; + Name = Resources.remotedesktop_title; + Id = "com.microsoft.cmdpal.builtin.remotedesktop"; + + _rdpConnectionManager = rdpConnectionManager; + } + + public override IListItem[] GetItems() => _rdpConnectionManager.Connections.ToArray(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4a6c84ddea --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests")] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..de0b924c33 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// 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 Microsoft.CmdPal.Ext.RemoteDesktop.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", "17.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("Microsoft.CmdPal.Ext.RemoteDesktop.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 Connect. + /// + public static string remotedesktop_command_connect { + get { + return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string remotedesktop_command_open { + get { + return ResourceManager.GetString("remotedesktop_command_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.. + /// + public static string remotedesktop_log_invalid_hostname { + get { + return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}. + /// + public static string remotedesktop_log_mstsc_error { + get { + return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to {0}. + /// + public static string remotedesktop_open_host { + get { + return ResourceManager.GetString("remotedesktop_open_host", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Remote Desktop Client. + /// + public static string remotedesktop_open_rdp { + get { + return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of connections to include in the query results by default. + /// + public static string remotedesktop_settings_predefined_connections_description { + get { + return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Predefined connections. + /// + public static string remotedesktop_settings_predefined_connections_title { + get { + return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Establish Remote Desktop connections. + /// + public static string remotedesktop_subtitle { + get { + return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remote Desktop. + /// + public static string remotedesktop_title { + get { + return ResourceManager.GetString("remotedesktop_title", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx new file mode 100644 index 0000000000..bfbf1d3ac5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Remote Desktop + + + Establish Remote Desktop connections + + + Open + + + Connect to {0} + + + Connect + + + Open Remote Desktop Client + + + Predefined connections + + + A list of connections to include in the query results by default + + + Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0} + + + The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address. + + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs new file mode 100644 index 0000000000..eefd467d5b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs @@ -0,0 +1,45 @@ +// 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.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Pages; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop; + +public partial class RemoteDesktopCommandProvider : CommandProvider +{ + private readonly CommandItem listPageCommand; + private readonly FallbackRemoteDesktopItem fallback; + + public RemoteDesktopCommandProvider() + { + Id = "com.microsoft.cmdpal.builtin.remotedesktop"; + DisplayName = Resources.remotedesktop_title; + Icon = Icons.RDPIcon; + + var settingsManager = new SettingsManager(); + var rdpConnectionsManager = new RDPConnectionsManager(settingsManager); + var listPage = new RemoteDesktopListPage(rdpConnectionsManager); + + fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager); + + listPageCommand = new CommandItem(listPage) + { + Subtitle = Resources.remotedesktop_subtitle, + Icon = Icons.RDPIcon, + MoreCommands = [ + new CommandContextItem(settingsManager.Settings.SettingsPage), + ], + }; + } + + public override ICommandItem[] TopLevelCommands() => [listPageCommand]; + + public override IFallbackCommandItem[] FallbackCommands() => [fallback]; +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs new file mode 100644 index 0000000000..dbca0d3833 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs @@ -0,0 +1,15 @@ +// 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.Collections.Generic; +using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +internal interface ISettingsInterface +{ + public IReadOnlyCollection PredefinedConnections { get; } + + public ToolkitSettings Settings { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs new file mode 100644 index 0000000000..1469e448d7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs @@ -0,0 +1,53 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +internal class SettingsManager : JsonSettingsManager, ISettingsInterface +{ + // Line break character used in WinUI3 TextBox and TextBlock. + private const char TEXTBOXNEWLINE = '\r'; + + private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private readonly TextSetting _predefinedConnections = new( + Namespaced(nameof(PredefinedConnections)), + Resources.remotedesktop_settings_predefined_connections_title, + Resources.remotedesktop_settings_predefined_connections_description, + string.Empty) + { + Multiline = true, + Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1", + }; + + public IReadOnlyCollection PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? []; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_predefinedConnections); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +}