CmdPal: New Remote Desktop built-in extension (#43090)

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 <niels.laute@live.nl>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
This commit is contained in:
Michael Jolley
2025-11-29 13:07:19 -06:00
committed by GitHub
parent 0de60445ea
commit 06afe09973
29 changed files with 1232 additions and 2 deletions

View File

@@ -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

View File

@@ -69,7 +69,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />

View File

@@ -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}

View File

@@ -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;

View File

@@ -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<ICommandProvider, BuiltInsCommandProvider>();
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
// Models
services.AddSingleton<TopLevelCommandManager>();

View File

@@ -118,6 +118,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />

View File

@@ -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);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<ConnectionListItem> _connections = new();
public IReadOnlyCollection<ConnectionListItem> Connections => _connections.AsReadOnly();
public MockRDPConnectionsManager(ISettingsInterface settingsManager)
{
_connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult));
}
}

View File

@@ -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<string> _connections;
public IReadOnlyCollection<string> PredefinedConnections => _connections;
public ToolkitSettings Settings { get; } = new();
public MockSettingsManager(params string[] predefinedConnections)
{
_connections = new(predefinedConnections);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,21 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3.55078" width="9.36537" height="9.36537" rx="0.720413" fill="url(#paint0_linear_2155_27162)"/>
<rect x="1" y="2" width="13" height="9" rx="0.722222" fill="url(#paint1_linear_2155_27162)"/>
<circle cx="11.4" cy="9.4" r="4.4" fill="url(#paint2_radial_2155_27162)"/>
<path d="M13.8703 11.2497C13.964 11.3434 14.116 11.3434 14.2097 11.2497C14.3034 11.156 14.3034 11.004 14.2097 10.9103L12.4594 9.16L14.2097 7.40971C14.3034 7.31598 14.3034 7.16402 14.2097 7.07029C14.116 6.97657 13.964 6.97657 13.8703 7.07029L11.9503 8.9903C11.8566 9.08402 11.8566 9.23598 11.9503 9.32971L13.8703 11.2497ZM9.40971 8.0303C9.31598 7.93657 9.16402 7.93657 9.07029 8.0303C8.97657 8.12402 8.97657 8.27598 9.07029 8.36971L10.8206 10.12L9.07029 11.8703C8.97657 11.964 8.97657 12.116 9.07029 12.2097C9.16402 12.3034 9.31598 12.3034 9.40971 12.2097L11.3297 10.2897C11.4234 10.196 11.4234 10.044 11.3297 9.95031L9.40971 8.0303Z" fill="#666666" stroke="#666666" stroke-width="0.146667"/>
<defs>
<linearGradient id="paint0_linear_2155_27162" x1="3.22298" y1="3.55078" x2="8.52487" y2="6.68847" gradientUnits="userSpaceOnUse">
<stop stop-color="#246FB0"/>
<stop offset="1" stop-color="#14518A"/>
</linearGradient>
<linearGradient id="paint1_linear_2155_27162" x1="1.15476" y1="1.66667" x2="14.867" y2="9.90553" gradientUnits="userSpaceOnUse">
<stop stop-color="#86D6F9"/>
<stop offset="1" stop-color="#1FA3E4"/>
</linearGradient>
<radialGradient id="paint2_radial_2155_27162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.9111 6.22222) rotate(90) scale(7.57778)">
<stop stop-color="#E7ECF1"/>
<stop offset="0.84" stop-color="#D2D4D6"/>
<stop offset="1" stop-color="#A9ABAC"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -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; }
}

View File

@@ -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);
}
}
}

View File

@@ -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(),
});
}
}
}

View File

@@ -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<ConnectionListItem> connections)
{
if (string.IsNullOrWhiteSpace(query))
{
return null;
}
var matchedConnection = ListHelpers.FilterList(
connections,
query,
(s, i) => ListHelpers.ScoreListItem(s, i))
.FirstOrDefault();
return matchedConnection;
}
}

View File

@@ -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<ConnectionListItem> Connections { get; }
}

View File

@@ -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<ConnectionListItem> _connections = new(Array.Empty<ConnectionListItem>());
private const int MinutesToCache = 1;
private DateTime? _connectionsLastLoaded;
public RDPConnectionsManager(ISettingsInterface settingsManager)
{
_settingsManager = settingsManager;
_settingsManager.Settings.SettingsChanged += (s, e) =>
{
_connectionsLastLoaded = null;
};
}
public IReadOnlyCollection<ConnectionListItem> Connections
{
get
{
if (!_connectionsLastLoaded.HasValue ||
(DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache)
{
var registryConnections = GetRdpConnectionsFromRegistry();
var predefinedConnections = GetPredefinedConnectionsFromSettings();
_connectionsLastLoaded = DateTime.Now;
var newConnections = new List<ConnectionListItem>(registryConnections.Count + predefinedConnections.Count + 1);
newConnections.AddRange(registryConnections);
newConnections.AddRange(predefinedConnections);
newConnections.Insert(0, _openRdpCommandListItem);
Interlocked.Exchange(ref _connections, new ReadOnlyCollection<ConnectionListItem>(newConnections));
}
return _connections;
}
}
private List<ConnectionListItem> GetRdpConnectionsFromRegistry()
{
using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default");
var validConnections = new List<ConnectionListItem>();
if (key is not null)
{
validConnections = key.GetValueNames()
.Select(name => key.GetValue(name))
.OfType<string>() // 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<ConnectionListItem> GetPredefinedConnectionsFromSettings()
{
var validConnections = _settingsManager.PredefinedConnections
.Select(s => s.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(ConnectionHelpers.MapToResult)
.ToList();
return validConnections;
}
}

View File

@@ -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");
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.RemoteDesktop.pri</ProjectPriFileName>
<nullable>enable</nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\RemoteDesktop.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RemoteDesktop.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -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();
}

View File

@@ -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")]

View File

@@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// </summary>
public static string remotedesktop_command_connect {
get {
return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
public static string remotedesktop_command_open {
get {
return ResourceManager.GetString("remotedesktop_command_open", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The hostname &apos;{0}&apos; was invalid. Ensure you&apos;re using a valid hostname or IP address..
/// </summary>
public static string remotedesktop_log_invalid_hostname {
get {
return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}.
/// </summary>
public static string remotedesktop_log_mstsc_error {
get {
return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connect to {0}.
/// </summary>
public static string remotedesktop_open_host {
get {
return ResourceManager.GetString("remotedesktop_open_host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Remote Desktop Client.
/// </summary>
public static string remotedesktop_open_rdp {
get {
return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A list of connections to include in the query results by default.
/// </summary>
public static string remotedesktop_settings_predefined_connections_description {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Predefined connections.
/// </summary>
public static string remotedesktop_settings_predefined_connections_title {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Establish Remote Desktop connections.
/// </summary>
public static string remotedesktop_subtitle {
get {
return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remote Desktop.
/// </summary>
public static string remotedesktop_title {
get {
return ResourceManager.GetString("remotedesktop_title", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="remotedesktop_title" xml:space="preserve">
<value>Remote Desktop</value>
</data>
<data name="remotedesktop_subtitle" xml:space="preserve">
<value>Establish Remote Desktop connections</value>
</data>
<data name="remotedesktop_command_open" xml:space="preserve">
<value>Open</value>
</data>
<data name="remotedesktop_open_host" xml:space="preserve">
<value>Connect to {0}</value>
</data>
<data name="remotedesktop_command_connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="remotedesktop_open_rdp" xml:space="preserve">
<value>Open Remote Desktop Client</value>
</data>
<data name="remotedesktop_settings_predefined_connections_title" xml:space="preserve">
<value>Predefined connections</value>
</data>
<data name="remotedesktop_settings_predefined_connections_description" xml:space="preserve">
<value>A list of connections to include in the query results by default</value>
</data>
<data name="remotedesktop_log_mstsc_error" xml:space="preserve">
<value>Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}</value>
</data>
<data name="remotedesktop_log_invalid_hostname" xml:space="preserve">
<value>The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.</value>
</data>
</root>

View File

@@ -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];
}

View File

@@ -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<string> PredefinedConnections { get; }
public ToolkitSettings Settings { get; }
}

View File

@@ -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<string> 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();
}
}