mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
CmdPal: Add "Uninstall Application" command (#41302)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Added the ability to uninstall UWP apps directly from the Command Palette (similar to the current Windows Start menu). For Win32 applications, the Windows Settings uninstall page is opened. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx > Not existing - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected > I messaged this to @michaeljolley in his Twitch chat, and he said it would be a cool feature. No further discussion has happened so far. - [x] **Tests:** Added/updated and all pass > No tests added, unsure which cases to cover > A run of the existing tests for this Package passed 100% - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places > Not required - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments I added a new file, `Commands/UninstallApplicationCommand.cs`, which implements the logic to uninstall applications directly from the Command Palette. The command differentiates between UWP apps and Win32 programs. All common error scenarios are properly handled and logged to ensure reliability and traceability. Additionally, in `Icons.cs`, I included the "Delete" icon from the Windows Start menu to be displayed alongside the uninstall commands in the Command List, providing a familiar visual cue for users. The uninstall commands have been integrated into the appropriate classes for both UWP and Win32 applications, making them fully accessible and consistent across the Command Palette. The command can be triggered using the shortcut Ctrl + Shift + Delete for quick access. <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tested locally; currently, verification was limited to uninstalling a UWP application, unsure of additional test scenarios. *Note: This is my first PR draft, so apologies if I missed anything.* --------- Co-authored-by: KnauerM <michael.knauer@rheinbahn.de>
This commit is contained in:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -70,6 +70,7 @@ APPMODEL
|
|||||||
APPNAME
|
APPNAME
|
||||||
appref
|
appref
|
||||||
appsettings
|
appsettings
|
||||||
|
appsfeatures
|
||||||
appwindow
|
appwindow
|
||||||
appwiz
|
appwiz
|
||||||
appxpackage
|
appxpackage
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// 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.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Windows.Management.Deployment;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.Apps.Commands;
|
||||||
|
|
||||||
|
internal sealed partial class UninstallApplicationCommand : InvokableCommand
|
||||||
|
{
|
||||||
|
// This is a ms-settings URI that opens the Apps & Features page in Windows Settings.
|
||||||
|
// It's correct and follows the Microsoft documentation:
|
||||||
|
// https://learn.microsoft.com/en-us/windows/apps/develop/launch/launch-settings-app#apps
|
||||||
|
private const string AppsFeaturesUri = "ms-settings:appsfeatures";
|
||||||
|
|
||||||
|
private readonly UWPApplication? _uwpTarget;
|
||||||
|
private readonly Win32Program? _win32Target;
|
||||||
|
|
||||||
|
public UninstallApplicationCommand(UWPApplication target)
|
||||||
|
{
|
||||||
|
Name = Resources.uninstall_application;
|
||||||
|
Icon = Icons.UninstallApplicationIcon;
|
||||||
|
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UninstallApplicationCommand(Win32Program target)
|
||||||
|
{
|
||||||
|
Name = Resources.uninstall_application;
|
||||||
|
Icon = Icons.UninstallApplicationIcon;
|
||||||
|
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CommandResult> UninstallUwpAppAsync(UWPApplication app)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(app.Package.FullName))
|
||||||
|
{
|
||||||
|
Logger.LogError($"Critical error while uninstalling: packageFullName cannot be null or empty.");
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Which timeout to use for the uninstallation operation?
|
||||||
|
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
|
||||||
|
{
|
||||||
|
var packageManager = new PackageManager();
|
||||||
|
var result = await packageManager.RemovePackageAsync(app.Package.FullName).AsTask(cts.Token);
|
||||||
|
|
||||||
|
if (result.ErrorText is not null && result.ErrorText.Length > 0)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to uninstall {app.Package.FullName}: {result.ErrorText}");
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update the Search results after uninstalling the app - unsure how to do this yet.
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_successful), app.DisplayName),
|
||||||
|
Result = CommandResult.GoHome(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Timeout exceeded while uninstalling {app.Package.FullName}");
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Permission denied to uninstall {app.Package.FullName}. Elevated privileges may be required. Error: {ex.Message}");
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"An unexpected error occurred during uninstallation of {app.Package.FullName}: {ex.Message}");
|
||||||
|
return CommandResult.ShowToast(new ToastArgs()
|
||||||
|
{
|
||||||
|
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
|
||||||
|
Result = CommandResult.KeepOpen(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override CommandResult Invoke()
|
||||||
|
{
|
||||||
|
if (_uwpTarget is not null)
|
||||||
|
{
|
||||||
|
return UninstallUwpAppAsync(_uwpTarget).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_win32Target is not null)
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = AppsFeaturesUri,
|
||||||
|
UseShellExecute = true,
|
||||||
|
});
|
||||||
|
return CommandResult.Dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogError("UninstallApplicationCommand invoked with no target.");
|
||||||
|
return CommandResult.Dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// 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.Text;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||||
|
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Ext.Apps.Commands;
|
||||||
|
|
||||||
|
internal sealed partial class UninstallApplicationConfirmation : InvokableCommand
|
||||||
|
{
|
||||||
|
private readonly UWPApplication? _uwpTarget;
|
||||||
|
private readonly Win32Program? _win32Target;
|
||||||
|
|
||||||
|
public UninstallApplicationConfirmation(UWPApplication target)
|
||||||
|
{
|
||||||
|
Name = Resources.uninstall_application;
|
||||||
|
Icon = Icons.UninstallApplicationIcon;
|
||||||
|
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UninstallApplicationConfirmation(Win32Program target)
|
||||||
|
{
|
||||||
|
Name = Resources.uninstall_application;
|
||||||
|
Icon = Icons.UninstallApplicationIcon;
|
||||||
|
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override CommandResult Invoke()
|
||||||
|
{
|
||||||
|
UninstallApplicationCommand uninstallCommand;
|
||||||
|
|
||||||
|
var applicationTitle = Resources.uninstall_application;
|
||||||
|
|
||||||
|
if (_uwpTarget is not null)
|
||||||
|
{
|
||||||
|
uninstallCommand = new UninstallApplicationCommand(_uwpTarget);
|
||||||
|
applicationTitle = _uwpTarget.DisplayName;
|
||||||
|
}
|
||||||
|
else if (_win32Target is not null)
|
||||||
|
{
|
||||||
|
uninstallCommand = new UninstallApplicationCommand(_win32Target);
|
||||||
|
applicationTitle = _win32Target.Name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError("UninstallApplicationCommand invoked with no target.");
|
||||||
|
return CommandResult.Dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmArgs = new ConfirmationArgs()
|
||||||
|
{
|
||||||
|
Title = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_confirm_title), applicationTitle),
|
||||||
|
Description = Resources.uninstall_application_confirm_description,
|
||||||
|
PrimaryCommand = uninstallCommand,
|
||||||
|
IsPrimaryCommandCritical = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return CommandResult.Confirm(confirmArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,6 @@ internal sealed class Icons
|
|||||||
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
|
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
|
||||||
|
|
||||||
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
|
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
|
||||||
|
|
||||||
|
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ public class UWPApplication : IUWPApplication
|
|||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
commands.Add(
|
||||||
|
new CommandContextItem(
|
||||||
|
new UninstallApplicationConfirmation(this))
|
||||||
|
{
|
||||||
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
|
||||||
|
IsCritical = true,
|
||||||
|
});
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,16 @@ public class Win32Program : IProgram
|
|||||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
|
||||||
|
{
|
||||||
|
commands.Add(new CommandContextItem(
|
||||||
|
new UninstallApplicationConfirmation(this))
|
||||||
|
{
|
||||||
|
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
|
||||||
|
IsCritical = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,51 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Uninstall.
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to This app and its related information will be removed..
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_confirm_description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_confirm_description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Uninstall "{0}"?.
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_confirm_title {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_confirm_title", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error while uninstalling '{0}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_failed {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_failed", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to '{0}' has been successfully uninstalled..
|
||||||
|
/// </summary>
|
||||||
|
internal static string uninstall_application_successful {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("uninstall_application_successful", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Unpin.
|
/// Looks up a localized string similar to Unpin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -198,6 +198,21 @@
|
|||||||
<data name="unpin_app" xml:space="preserve">
|
<data name="unpin_app" xml:space="preserve">
|
||||||
<value>Unpin</value>
|
<value>Unpin</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="uninstall_application" xml:space="preserve">
|
||||||
|
<value>Uninstall</value>
|
||||||
|
</data>
|
||||||
|
<data name="uninstall_application_successful" xml:space="preserve">
|
||||||
|
<value>'{0}' has been successfully uninstalled.</value>
|
||||||
|
</data>
|
||||||
|
<data name="uninstall_application_failed" xml:space="preserve">
|
||||||
|
<value>Error while uninstalling '{0}'</value>
|
||||||
|
</data>
|
||||||
|
<data name="uninstall_application_confirm_description" xml:space="preserve">
|
||||||
|
<value>This app and its related information will be removed.</value>
|
||||||
|
</data>
|
||||||
|
<data name="uninstall_application_confirm_title" xml:space="preserve">
|
||||||
|
<value>Uninstall "{0}"?</value>
|
||||||
|
</data>
|
||||||
<data name="limit_1" xml:space="preserve">
|
<data name="limit_1" xml:space="preserve">
|
||||||
<value>1</value>
|
<value>1</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
Reference in New Issue
Block a user