diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index f00870c9283..40a6a085f5e 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -745,6 +745,7 @@ Kybd
languagesjson
lastcodeanalysissucceeded
Lastdevice
+LASTEXITCODE
LAYOUTRTL
LCIDTo
lcl
@@ -917,6 +918,7 @@ MSIFASTINSTALL
MSIHANDLE
msiquery
MSIRESTARTMANAGERCONTROL
+msixbundle
MSIXCA
MSLLHOOKSTRUCT
Mso
diff --git a/installer/PowerToysSetup/Settings.wxs b/installer/PowerToysSetup/Settings.wxs
index b9c27b00c4f..a31903d643b 100644
--- a/installer/PowerToysSetup/Settings.wxs
+++ b/installer/PowerToysSetup/Settings.wxs
@@ -54,6 +54,9 @@
+
+
+
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/CheckCmdNotFoundRequirements.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/CheckCmdNotFoundRequirements.ps1
new file mode 100644
index 00000000000..260d75c5334
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/CheckCmdNotFoundRequirements.ps1
@@ -0,0 +1,41 @@
+Write-Host $PSVersionTable
+if ($PSVersionTable.PSVersion -ge 7.4)
+{
+ Write-Host "PowerShell 7.4 or greater detected."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+else
+{
+ Write-Host "PowerShell 7.4 or greater not detected. Installation instructions can be found on https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows `r`n"
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+
+if (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)
+{
+ Write-Host "WinGet Client module detected."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+else {
+ Write-Host "WinGet Client module not detected. Installation instructions can be found on https://www.powershellgallery.com/packages/Microsoft.WinGet.Client `r`n"
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+
+if (!(Test-Path $PROFILE))
+{
+ Write-Host "Profile file $PROFILE not found".
+ New-Item -Path $PROFILE -ItemType File
+ Write-Host "Created profile file $PROFILE".
+}
+
+$profileContent = Get-Content -Path $PROFILE -Raw
+
+if ((-not [string]::IsNullOrEmpty($profileContent)) -and ($profileContent.Contains("34de4b3d-13a8-4540-b76d-b9e8d3851756")))
+{
+ Write-Host "Command Not Found module is registered in the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+else
+{
+ Write-Host "Command Not Found module is not registered in the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1
index fef86f49359..501c4c4141e 100644
--- a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/DisableModule.ps1
@@ -30,6 +30,8 @@ if($atLeastOneInstanceFound)
{
Set-Content -Path $PROFILE -Value $newContent
Write-Host "Removed the Command Not Found reference from the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
} else {
Write-Host "No instance of Command Not Found was found in the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1
index 844613a2be4..3b4842dba14 100644
--- a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/EnableModule.ps1
@@ -28,6 +28,7 @@ $profileContent = Get-Content -Path $PROFILE -Raw
if ((-not [string]::IsNullOrEmpty($profileContent)) -and ($profileContent.Contains("34de4b3d-13a8-4540-b76d-b9e8d3851756")))
{
Write-Host "Module is already registered in the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
}
else
{
@@ -35,4 +36,5 @@ else
Add-Content -Path $PROFILE -Value "`r`nImport-Module `"$scriptPath\WinGetCommandNotFound.psd1`""
Add-Content -Path $PROFILE -Value "#34de4b3d-13a8-4540-b76d-b9e8d3851756"
Write-Host "Module was successfully registered in the profile file."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallPowerShell7.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallPowerShell7.ps1
new file mode 100644
index 00000000000..a6af1c66e2e
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallPowerShell7.ps1
@@ -0,0 +1,81 @@
+
+if ((Get-AppxPackage microsoft.DesktopAppInstaller).Version -ge [System.Version]"1.21")
+{
+ Write-Host "Detected winget. Will try to install PowerShell."
+}
+else
+{
+ Write-Host "WinGet not detected. Will try to install."
+ # To speed up Invoke-WebRequest. Older versions are very slow when printing the progress.
+ $ProgressPreference = 'SilentlyContinue'
+ $cpuArchitecture="x64"
+ $detectedArchitecture=""
+ if ($env:PROCESSOR_ARCHITEW6432 -eq $null) {
+ $detectedArchitecture=$env:PROCESSOR_ARCHITECTURE
+ } else {
+ $detectedArchitecture=$env:PROCESSOR_ARCHITEW6432
+ }
+ Write-Host "Detected the CPU architecture:$detectedArchitecture"
+ if ($detectedArchitecture -ne "AMD64")
+ {
+ Write-Host "Mismatch with AMD64, setting it to arm64, since that's where we're likely running."
+ $cpuArchitecture="arm64"
+ }
+ if((Get-AppxPackage Microsoft.VCLibs.140.00).Version -ge [System.Version]"14.0.30704")
+ {
+ Write-Host "Detected Microsoft.VCLibs.140.00."
+ }
+ else
+ {
+ Write-Host "Microsoft.VCLibs.140.00 not detected. Will try to install."
+ Invoke-WebRequest -Uri https://aka.ms/Microsoft.VCLibs.$cpuArchitecture.14.00.Desktop.appx -OutFile "$Env:TMP\Microsoft.VCLibs.14.00.appx"
+ Add-AppxPackage -Path "$Env:TMP\Microsoft.VCLibs.14.00.appx"
+ Remove-Item -Path "$Env:TMP\Microsoft.VCLibs.14.00.appx" -Force
+ }
+ if((Get-AppxPackage Microsoft.VCLibs.140.00.UWPDesktop).Version -ge [System.Version]"14.0.30704")
+ {
+ Write-Host "Detected Microsoft.VCLibs.140.00.UWPDesktop"
+ }
+ else
+ {
+ Write-Host "Microsoft.VCLibs.140.00.UWPDesktop not detected. Will try to install."
+ Invoke-WebRequest -Uri https://aka.ms/Microsoft.VCLibs.$cpuArchitecture.14.00.Desktop.appx -OutFile "$Env:TMP\Microsoft.VCLibs.14.00.Desktop.appx"
+ Add-AppxPackage -Path "$Env:TMP\Microsoft.VCLibs.14.00.Desktop.appx"
+ Remove-Item -Path "$Env:TMP\Microsoft.VCLibs.14.00.Desktop.appx" -Force
+ }
+ if (Get-AppxPackage Microsoft.UI.Xaml.2.7)
+ {
+ Write-Host "Detected Microsoft.UI.Xaml.2.7"
+ }
+ else
+ {
+ Write-Host "Microsoft.UI.Xaml.2.7 not detected. Will try to install."
+ Write-Host "Downloading to $Env:TMP\microsoft.ui.xaml.2.7.3.zip"
+ Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Microsoft.UI.Xaml/2.7.3 -OutFile "$Env:TMP\microsoft.ui.xaml.2.7.3.zip"
+ Write-Host "Extracting $Env:TMP\microsoft.ui.xaml.2.7.3.zip"
+ Expand-Archive "$Env:TMP\microsoft.ui.xaml.2.7.3.zip" -DestinationPath "$Env:TMP\microsoft.ui.xaml.2.7.3"
+ Write-Host "Installing $Env:TMP\microsoft.ui.xaml.2.7.3\tools\AppX\$cpuArchitecture\Release\Microsoft.UI.Xaml.2.7.appx"
+ Add-AppxPackage "$Env:TMP\microsoft.ui.xaml.2.7.3\tools\AppX\$cpuArchitecture\Release\Microsoft.UI.Xaml.2.7.appx"
+ Remove-Item -Path "$Env:TMP\microsoft.ui.xaml.2.7.3" -Recurse -Force
+ Remove-Item -Path "$Env:TMP\microsoft.ui.xaml.2.7.3.zip" -Force
+ }
+ Write-Host "Getting winget to the latest stable"
+ Invoke-WebRequest -Uri https://aka.ms/getwinget -OutFile "$Env:TMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
+ Add-AppxPackage -Path "$Env:TMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
+ Remove-Item -Path "$Env:TMP\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -Force
+
+ #winget is not visible right away, so reload the PATH variable.
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
+}
+
+winget install Microsoft.PowerShell --source winget
+if ($LASTEXITCODE -eq 0)
+{
+ Write-Host "Powershell 7 successfully installed."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+else
+{
+ Write-Host "Powershell 7 was not installed."
+}
+
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallWinGetClientModule.ps1 b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallWinGetClientModule.ps1
new file mode 100644
index 00000000000..ed48ac84e40
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Assets/Settings/Scripts/InstallWinGetClientModule.ps1
@@ -0,0 +1,16 @@
+if (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)
+{
+ Write-Host "WinGet Client module detected."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+}
+else {
+ Install-Module -Name Microsoft.WinGet.Client
+ if (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)
+ {
+ Write-Host "WinGet Client module detected."
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+ } else {
+ Write-Host "WinGet Client module not detected. Installation instructions can be found on https://www.powershellgallery.com/packages/Microsoft.WinGet.Client `r`n"
+ # This message will be compared against in Command Not Found Settings page code behind. Take care when changing it.
+ }
+}
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index 16c118872a4..90a9709dfb1 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -119,19 +119,28 @@
VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101
-
-
- Always
-
-
+
+
+ Always
+
+
-
-
- Always
-
-
- Always
-
-
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml
index d08cea71aae..2ba83a2a0b6 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCmdNotFound.xaml
@@ -12,10 +12,10 @@
-
+
-
+
-
+
+
+
+
@@ -20,25 +27,115 @@
IsTabStop="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured}"
Severity="Informational" />
-
-
-
-
-
-
-
-
-
-
-
-
+ IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabledGpoConfigured, Converter={StaticResource BoolNegationConverter}}"
+ IsExpanded="True">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Command Not Found"Command Not Found" is a product name
+
+
+ Installed
+
+
+ Detected
+
+
+ Not detected
+
+
+ The following components are required
+
+
+ PowerShell 7.4 or greater
+
+
+ WinGet Client PowerShell moduleCommand Not Found"Command Not Found" is a product name
-
+
Add this module to the PowerShell 7 profile script so that it is enabled with every new session
@@ -3691,7 +3709,10 @@ Activate by holding the key for the character you want to add an accent to, then
PowerShell 7 is required to use this module
- Check if your PowerShell configuration is compatible
+ Refresh
+
+
+ Check if your PowerShell configuration is compatible and configured correctlyInstall
diff --git a/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs
index e814354788a..712194b453e 100644
--- a/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/CmdNotFoundViewModel.cs
@@ -7,6 +7,7 @@
using System.IO;
using System.Reflection;
using global::PowerToys.GPOWrapper;
+using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
@@ -16,7 +17,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class CmdNotFoundViewModel : Observable
{
- public ButtonClickCommand CheckPowershellVersionEventHandler => new ButtonClickCommand(CheckPowershellVersion);
+ public ButtonClickCommand CheckRequirementsEventHandler => new ButtonClickCommand(CheckCommandNotFoundRequirements);
+
+ public ButtonClickCommand InstallPowerShell7EventHandler => new ButtonClickCommand(InstallPowerShell7);
+
+ public ButtonClickCommand InstallWinGetClientModuleEventHandler => new ButtonClickCommand(InstallWinGetClientModule);
public ButtonClickCommand InstallModuleEventHandler => new ButtonClickCommand(InstallModule);
@@ -49,6 +54,8 @@ private void InitializeEnabledValue()
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
}
+
+ CheckCommandNotFoundRequirements();
}
private string _commandOutputLog;
@@ -66,20 +73,66 @@ public string CommandOutputLog
}
}
+ private bool _isPowerShell7Detected;
+
+ public bool IsPowerShell7Detected
+ {
+ get => _isPowerShell7Detected;
+ set
+ {
+ if (_isPowerShell7Detected != value)
+ {
+ _isPowerShell7Detected = value;
+ OnPropertyChanged(nameof(IsPowerShell7Detected));
+ }
+ }
+ }
+
+ private bool _isWinGetClientModuleDetected;
+
+ public bool IsWinGetClientModuleDetected
+ {
+ get => _isWinGetClientModuleDetected;
+ set
+ {
+ if (_isWinGetClientModuleDetected != value)
+ {
+ _isWinGetClientModuleDetected = value;
+ OnPropertyChanged(nameof(IsWinGetClientModuleDetected));
+ }
+ }
+ }
+
+ private bool _isCommandNotFoundModuleInstalled;
+
+ public bool IsCommandNotFoundModuleInstalled
+ {
+ get => _isCommandNotFoundModuleInstalled;
+ set
+ {
+ if (_isCommandNotFoundModuleInstalled != value)
+ {
+ _isCommandNotFoundModuleInstalled = value;
+ OnPropertyChanged(nameof(IsCommandNotFoundModuleInstalled));
+ }
+ }
+ }
+
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
- public void RunPowerShellScript(string powershellArguments)
+ public string RunPowerShellScript(string powershellExecutable, string powershellArguments, bool hidePowerShellWindow = false)
{
string outputLog = string.Empty;
try
{
var startInfo = new ProcessStartInfo()
{
- FileName = "pwsh.exe",
+ FileName = powershellExecutable,
Arguments = powershellArguments,
+ CreateNoWindow = hidePowerShellWindow,
UseShellExecute = false,
RedirectStandardOutput = true,
};
@@ -96,28 +149,112 @@ public void RunPowerShellScript(string powershellArguments)
}
CommandOutputLog = outputLog;
+ return outputLog;
}
- public void CheckPowershellVersion()
+ public void CheckCommandNotFoundRequirements()
{
- var arguments = $"-NoProfile -NonInteractive -Command $PSVersionTable";
- RunPowerShellScript(arguments);
+ var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\CheckCmdNotFoundRequirements.ps1";
+ var arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Unrestricted -File \"{ps1File}\"";
+ var result = RunPowerShellScript("pwsh.exe", arguments, true);
+
+ if (result.Contains("PowerShell 7.4 or greater detected."))
+ {
+ IsPowerShell7Detected = true;
+ }
+ else if (result.Contains("PowerShell 7.4 or greater not detected."))
+ {
+ IsPowerShell7Detected = false;
+ }
+ else if (result.Contains("pwsh.exe"))
+ {
+ // Likely an error saying there was an error starting pwsh.exe, so we can assume Powershell 7 was not detected.
+ CommandOutputLog += "PowerShell 7.4 or greater not detected. Installation instructions can be found on https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows \r\n";
+ IsPowerShell7Detected = false;
+ }
+
+ if (result.Contains("WinGet Client module detected."))
+ {
+ IsWinGetClientModuleDetected = true;
+ }
+ else if (result.Contains("WinGet Client module not detected."))
+ {
+ IsWinGetClientModuleDetected = false;
+ }
+
+ if (result.Contains("Command Not Found module is registered in the profile file."))
+ {
+ IsCommandNotFoundModuleInstalled = true;
+ }
+ else if (result.Contains("Command Not Found module is not registered in the profile file."))
+ {
+ IsCommandNotFoundModuleInstalled = false;
+ }
+
+ Logger.LogInfo(result);
+ }
+
+ public void InstallPowerShell7()
+ {
+ var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\InstallPowerShell7.ps1";
+ var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\"";
+ var result = RunPowerShellScript("powershell.exe", arguments);
+ if (result.Contains("Powershell 7 successfully installed."))
+ {
+ IsPowerShell7Detected = true;
+ }
+
+ Logger.LogInfo(result);
+
+ // Update PATH environment variable to get pwsh.exe on further calls.
+ Environment.SetEnvironmentVariable("PATH", (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty) + ";" + (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty), EnvironmentVariableTarget.Process);
+ }
+
+ public void InstallWinGetClientModule()
+ {
+ var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\InstallWinGetClientModule.ps1";
+ var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\"";
+ var result = RunPowerShellScript("pwsh.exe", arguments);
+ if (result.Contains("WinGet Client module detected."))
+ {
+ IsWinGetClientModuleDetected = true;
+ }
+ else if (result.Contains("WinGet Client module not detected."))
+ {
+ IsWinGetClientModuleDetected = false;
+ }
+
+ Logger.LogInfo(result);
}
public void InstallModule()
{
var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\EnableModule.ps1";
var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\" -scriptPath \"{AssemblyDirectory}\\..\"";
- RunPowerShellScript(arguments);
- PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundInstallEvent());
+ var result = RunPowerShellScript("pwsh.exe", arguments);
+
+ if (result.Contains("Module is already registered in the profile file.") || result.Contains("Module was successfully registered in the profile file."))
+ {
+ IsCommandNotFoundModuleInstalled = true;
+ PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundInstallEvent());
+ }
+
+ Logger.LogInfo(result);
}
public void UninstallModule()
{
var ps1File = AssemblyDirectory + "\\Assets\\Settings\\Scripts\\DisableModule.ps1";
var arguments = $"-NoProfile -ExecutionPolicy Unrestricted -File \"{ps1File}\"";
- RunPowerShellScript(arguments);
- PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundUninstallEvent());
+ var result = RunPowerShellScript("pwsh.exe", arguments);
+
+ if (result.Contains("Removed the Command Not Found reference from the profile file.") || result.Contains("No instance of Command Not Found was found in the profile file."))
+ {
+ IsCommandNotFoundModuleInstalled = false;
+ PowerToysTelemetry.Log.WriteEvent(new CmdNotFoundUninstallEvent());
+ }
+
+ Logger.LogInfo(result);
}
- }
+ }
}