Compare commits

..
13 changed files with 101 additions and 67 deletions
+2
View File
@@ -134,10 +134,12 @@
"ApplyCompletionTitleSuccess": "Changes Applied",
"ApplyCompletionMessageDefault": "Please note that some changes will only take effect after a reboot. Thanks for using Win11Debloat!",
"ApplyRebootRequiredHeader": "A reboot is required for these changes to take effect:",
"ApplyRebootRequiredWingetDeferred": "Uninstallation of {0}",
"ApplyCompletionTitleCancelled": "Cancelled",
"ApplyCompletionMessageCancelled": "Script execution was cancelled by the user.",
"ApplyCompletionTitleErrors": "Changes Applied with Errors",
"ApplyCompletionMessageVerificationUnavailable": "All changes were applied without errors, but Win11Debloat could not confirm that all selected apps were successfully uninstalled.",
"WingetUserScopeUninstallDeferred": "WinGet could not uninstall {0} while running as administrator because it is installed for user scope. Restart the PC or sign out and back in as {1} to complete removal.",
"ApplyCompletionMessageFailures_one": "{0} change failed. See console for details.",
"ApplyCompletionMessageFailures_other": "{0} changes failed. See console for details.",
"ApplyCompletionMessageReady": "Your system is ready. Thanks for using Win11Debloat!",
-7
View File
@@ -202,13 +202,6 @@
"UndoLabel": "Enable AI features in Microsoft Edge",
"ApplyUndoText": "Enabling AI features in Microsoft Edge"
},
"DisableChromeAI": {
"Label": "Disable AI features in Google Chrome",
"ToolTip": "Disables Google Chrome's built-in AI tools and integrations, including Gemini, AI search & suggestions, writing assistance, autofill and more. It also removes local AI models. This feature uses policies, which will lock down certain settings.",
"ApplyText": "Disabling AI features in Google Chrome",
"UndoLabel": "Enable AI features in Google Chrome",
"ApplyUndoText": "Enabling AI features in Google Chrome"
},
"DisablePaintAI": {
"Label": "Disable AI features in Paint",
"ToolTip": "This will turn off AI features in Paint, such as the AI-powered image generation and editing tools. This feature uses policies, which will lock down certain settings.",
+2
View File
@@ -125,10 +125,12 @@
"ApplyCompletionTitleSuccess": "Modifiche applicate",
"ApplyCompletionMessageDefault": "Nota che alcune modifiche avranno effetto solo dopo il riavvio. Grazie per aver usato Win11Debloat!",
"ApplyRebootRequiredHeader": "È necessario riavviare il sistema affinché le seguenti modifiche abbiano effetto:",
"ApplyRebootRequiredWingetDeferred": "Disinstallazione di {0}",
"ApplyCompletionTitleCancelled": "Annullato",
"ApplyCompletionMessageCancelled": "L'esecuzione dello script è stata annullata dall'utente.",
"ApplyCompletionTitleErrors": "Modifiche applicate con errori",
"ApplyCompletionMessageVerificationUnavailable": "Tutte le modifiche sono state applicate senza errori, ma Win11Debloat non ha potuto confermare che tutte le app selezionate siano state disinstallate correttamente.",
"WingetUserScopeUninstallDeferred": "WinGet non ha potuto disinstallare {0} con privilegi di amministratore perché è installata per l'utente. Riavvia il PC oppure esci e accedi di nuovo come {1} per completare la rimozione.",
"ApplyCompletionMessageFailures_one": "{0} modifica non riuscita. Consulta la console per i dettagli.",
"ApplyCompletionMessageFailures_other": "{0} modifiche non riuscite. Consulta la console per i dettagli.",
"ApplyCompletionMessageReady": "Il sistema è pronto. Grazie per aver usato Win11Debloat!",
+3 -1
View File
@@ -124,11 +124,13 @@
"ApplyCreatingRestorePoint": "Systeemherstelpunt maken. Dit kan even duren...",
"ApplyCompletionTitleSuccess": "Wijzigingen toegepast",
"ApplyCompletionMessageDefault": "Sommige wijzigingen worden pas actief nadat je de computer opnieuw hebt opgestart. Bedankt voor het gebruik van Win11Debloat!",
"ApplyRebootRequiredHeader": "Start de computer opnieuw op om de volgende wijzigingen te activeren:",
"ApplyRebootRequiredHeader": "De volgende wijzigingen vereisen een herstart:",
"ApplyRebootRequiredWingetDeferred": "Verwijderen van {0}",
"ApplyCompletionTitleCancelled": "Geannuleerd",
"ApplyCompletionMessageCancelled": "Het toepassen van de wijzigingen is geannuleerd.",
"ApplyCompletionTitleErrors": "Wijzigingen toegepast met fouten",
"ApplyCompletionMessageVerificationUnavailable": "Er zijn geen fouten gemeld, maar Win11Debloat kon niet controleren of alle geselecteerde apps zijn verwijderd.",
"WingetUserScopeUninstallDeferred": "WinGet kon {0} niet verwijderen als administrator, omdat de app voor een gebruiker is geïnstalleerd. Start de pc opnieuw op of meld je af en weer aan als {1} om de verwijdering te voltooien.",
"ApplyCompletionMessageFailures_one": "{0} wijziging is mislukt. Zie de console voor meer informatie.",
"ApplyCompletionMessageFailures_other": "{0} wijzigingen zijn mislukt. Zie de console voor meer informatie.",
"ApplyCompletionMessageReady": "Je systeem is gereed. Bedankt voor het gebruik van Win11Debloat!",
+2
View File
@@ -134,10 +134,12 @@
"ApplyCompletionTitleSuccess": "Alterações aplicadas",
"ApplyCompletionMessageDefault": "Observe que algumas alterações só terão efeito após uma reinicialização. Obrigado por usar o Win11Debloat!",
"ApplyRebootRequiredHeader": "Uma reinicialização é necessária para que estas alterações tenham efeito:",
"ApplyRebootRequiredWingetDeferred": "Desinstalação de {0}",
"ApplyCompletionTitleCancelled": "Cancelado",
"ApplyCompletionMessageCancelled": "A execução do script foi cancelada pelo usuário.",
"ApplyCompletionTitleErrors": "Alterações aplicadas com erros",
"ApplyCompletionMessageVerificationUnavailable": "Todas as alterações foram aplicadas sem erros, mas o Win11Debloat não conseguiu confirmar que todos os apps selecionados foram desinstalados com sucesso.",
"WingetUserScopeUninstallDeferred": "O WinGet não conseguiu desinstalar {0} com privilégios de administrador porque ele está instalado para o usuário. Reinicie o PC ou saia e entre novamente como {1} para concluir a remoção.",
"ApplyCompletionMessageFailures_one": "{0} alteração falhou. Veja o console para detalhes.",
"ApplyCompletionMessageFailures_other": "{0} alterações falharam. Veja o console para detalhes.",
"ApplyCompletionMessageFailures_many": "{0} de alterações falharam. Veja o console para detalhes.",
+6 -11
View File
@@ -157,23 +157,18 @@
<Setter Property="Margin" Value="8,0,8,0"/>
</Style>
<!-- Column widths for the app table rows and header (indicator | name | description | id) -->
<!-- Column widths for the app table rows and header (dot | name | description | id) -->
<GridLength x:Key="AppTableDotColWidth">16</GridLength>
<GridLength x:Key="AppTableNameColWidth">180</GridLength>
<GridLength x:Key="AppTableDescColWidth">1*</GridLength>
<GridLength x:Key="AppTableIdColWidth">261</GridLength>
<!-- Recommendation indicator style shared by circles, diamonds, and triangles -->
<Style x:Key="AppRecommendationIndicatorStyle" TargetType="Shape">
<!-- Recommendation dot shape style for app table rows -->
<Style x:Key="AppRecommendationDotStyle" TargetType="Ellipse">
<Setter Property="Width" Value="9"/>
<Setter Property="Height" Value="9"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="RenderTransform">
<Setter.Value>
<TranslateTransform Y="1"/>
</Setter.Value>
</Setter>
</Style>
<!-- Container style for each dynamically-created app table row -->
@@ -189,7 +184,7 @@
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
<!-- App recommendation indicator / legend colors -->
<!-- App recommendation dot / legend colors -->
<SolidColorBrush x:Key="AppRecommendationSafeColor" Color="#4CAF50"/>
<SolidColorBrush x:Key="AppRecommendationOptionalColor" Color="#FFC107"/>
<SolidColorBrush x:Key="AppRecommendationUnsafeColor" Color="#F44336"/>
@@ -715,12 +710,12 @@
</StackPanel>
<!-- Optional (default) -->
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="%LANG:AppLegendOptionalTooltip%">
<Polygon Points="4.5,0 9,4.5 4.5,9 0,4.5" Stretch="Fill" Width="9" Height="9" Fill="{DynamicResource AppRecommendationOptionalColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationOptionalColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="%LANG:AppLegendOptional%" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
<!-- Not Recommended (unsafe) -->
<StackPanel Orientation="Horizontal" ToolTip="%LANG:AppLegendNotRecommendedTooltip%">
<Polygon Points="0,9 4.5,0 9,9" Stretch="Fill" Width="9" Height="9" Fill="{DynamicResource AppRecommendationUnsafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationUnsafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="%LANG:AppLegendNotRecommended%" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
</StackPanel>
+37 -7
View File
@@ -43,6 +43,7 @@ function Remove-SelectedApps {
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$wingetRemovedApps = @()
$wingetRemovalFailures = @{}
if (-not $script:WingetDeferredRemovals) { $script:WingetDeferredRemovals = @{} }
Foreach ($app in $appsList) {
if ($script:CancelRequested) { return $false }
@@ -74,6 +75,10 @@ function Remove-SelectedApps {
}
# Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) {
$wingetRemovedApps = @($wingetRemovedApps | Where-Object { -not $script:WingetDeferredRemovals.ContainsKey($_) })
}
if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
@@ -122,10 +127,10 @@ function Remove-SelectedApps {
Runs winget uninstall for a single app, with a bounded execution time.
WinGet's own exit code/success reporting is unreliable and is only logged
for diagnostics; it never causes this function to report failure. Callers
verify removal with a post-removal inventory check instead. This function
only reports failure when the winget invocation itself throws a terminating
error (e.g. it times out or cannot be started). If the User or Sysprep
parameter was passed, also schedules removal for future logins.
verify removal with a post-removal inventory check instead. If WinGet blocks
an elevated uninstall of a user-scope package, removal is deferred to the
target user's next logon. If the User or Sysprep parameter was passed, this
function also schedules removal for future logins.
.PARAMETER app
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
@@ -151,6 +156,7 @@ function Remove-WinGetApp {
$uninstallCommandSucceeded = $true
$exitCode = $null
$uninstallDeferred = $false
try {
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
param($appId)
@@ -163,6 +169,25 @@ function Remove-WinGetApp {
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode."
# WinGet reports APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED (0x8A15007D).
# Keep the English message check as a fallback for clients that don't return the specific code.
$userScopeBlocked = ([string]$exitCode -eq '-1978335107') -or ([string]$exitCode -match '^0x0?8A15007D$')
if (-not $userScopeBlocked) {
$userScopeBlocked = @($uninstallResult.Output | Where-Object {
$null -ne $_ -and $_.ToString() -match 'package installed for user scope cannot be uninstalled when running with administrator privileges'
}).Count -gt 0
}
if ($userScopeBlocked) {
$targetUserName = Get-RunOnceWingetTargetUserName
$uninstallDeferred = Set-RunOnceWingetTask -appId $app
if ($uninstallDeferred) {
if (-not $script:WingetDeferredRemovals) { $script:WingetDeferredRemovals = @{} }
$script:WingetDeferredRemovals[$app] = $targetUserName
Write-Host (Get-Translation -Key 'WingetUserScopeUninstallDeferred' -FormatArgs @($app, $targetUserName)) -ForegroundColor Yellow
}
}
}
catch {
$uninstallCommandSucceeded = $false
@@ -175,11 +200,11 @@ function Remove-WinGetApp {
}
$scheduleSucceeded = $true
if ($script:Params.ContainsKey("User")) {
if (-not $uninstallDeferred -and $script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall $app for user $(Get-UserName)..."
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
elseif ($script:Params.ContainsKey("Sysprep")) {
elseif (-not $uninstallDeferred -and $script:Params.ContainsKey("Sysprep")) {
Write-Host "Adding scheduled task to uninstall $app for new users..."
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
@@ -343,6 +368,11 @@ function Request-EdgeForceRemove {
return $false
}
function Get-RunOnceWingetTargetUserName {
if ($script:Params.ContainsKey("Sysprep")) { return "Default" }
return Get-UserName
}
<#
.SYNOPSIS
Dynamically sets a RunOnce registry key to schedule a winget uninstall.
@@ -364,7 +394,7 @@ function Request-EdgeForceRemove {
function Set-RunOnceWingetTask {
param([string]$appId)
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
$targetUserName = Get-RunOnceWingetTargetUserName
# Sanitize appId for use in registry value names (backslashes are path separators)
$safeAppId = $appId.Replace('\', '_')
+1
View File
@@ -351,6 +351,7 @@ function Invoke-AllChanges {
$script:AppRemovalFailures = 0
$script:FeatureFailures = 0
$script:AppRemovalVerificationUnavailable = $false
$script:WingetDeferredRemovals = @{}
# ---- Gather work items ----
$applyIds = @()
+7 -21
View File
@@ -431,7 +431,7 @@ function Add-AppsToMainWindow {
$checkbox.IsChecked = $app.IsChecked
$checkbox.Style = $Window.Resources['AppsPanelCheckBoxStyle']
# Build table row: Recommendation indicator | Name | Description | App ID
# Build table row: Recommendation dot | Name | Description | App ID
$row = New-Object System.Windows.Controls.Grid
$row.Style = $Window.Resources['AppTableRowStyle']
$c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $Window.Resources['AppTableDotColWidth']
@@ -441,30 +441,16 @@ function Add-AppsToMainWindow {
$row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1)
$row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3)
$indicator = switch ($app.Recommendation) {
'safe' { New-Object System.Windows.Shapes.Ellipse }
'unsafe' {
$triangle = New-Object System.Windows.Shapes.Polygon
$triangle.Points = [System.Windows.Media.PointCollection]::Parse('0,9 4.5,0 9,9')
$triangle.Stretch = 'Fill'
$triangle
}
default {
$diamond = New-Object System.Windows.Shapes.Polygon
$diamond.Points = [System.Windows.Media.PointCollection]::Parse('4.5,0 9,4.5 4.5,9 0,4.5')
$diamond.Stretch = 'Fill'
$diamond
}
}
$indicator.Style = $Window.Resources['AppRecommendationIndicatorStyle']
$indicator.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
$dot = New-Object System.Windows.Shapes.Ellipse
$dot.Style = $Window.Resources['AppRecommendationDotStyle']
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
$recommendationKey = switch ($app.Recommendation) {
'safe' { 'AppLegendRecommendedTooltip' }
'unsafe' { 'AppLegendNotRecommendedTooltip' }
default { 'AppLegendOptionalTooltip' }
}
$indicator.SetResourceReference([System.Windows.FrameworkElement]::ToolTipProperty, "Language_$recommendationKey")
[System.Windows.Controls.Grid]::SetColumn($indicator, 0)
$dot.SetResourceReference([System.Windows.FrameworkElement]::ToolTipProperty, "Language_$recommendationKey")
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
$tbName = New-Object System.Windows.Controls.TextBlock
$tbName.Text = $app.FriendlyName
@@ -483,7 +469,7 @@ function Add-AppsToMainWindow {
$tbId.ToolTip = $app.AppIdDisplay
[System.Windows.Controls.Grid]::SetColumn($tbId, 3)
$row.Children.Add($indicator) | Out-Null
$row.Children.Add($dot) | Out-Null
$row.Children.Add($tbName) | Out-Null
$row.Children.Add($tbDesc) | Out-Null
$row.Children.Add($tbId) | Out-Null
+15
View File
@@ -184,6 +184,21 @@ function Show-ApplyModal {
}
}
}
if ($script:WingetDeferredRemovals -and $script:WingetDeferredRemovals.Count -gt 0) {
foreach ($appId in @($script:WingetDeferredRemovals.Keys | Sort-Object)) {
$tb = [System.Windows.Controls.TextBlock]::new()
$appName = Get-Translation -Key $appId -Field 'FriendlyName' -Section 'Apps'
$label = Get-Translation -Key 'ApplyRebootRequiredWingetDeferred' -FormatArgs @($appName)
$tb.Text = "$([char]0x2022) $label"
$tb.FontSize = 12
$tb.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, "AppFgColor")
$tb.Opacity = 0.85
$tb.Margin = [System.Windows.Thickness]::new(0, 2, 0, 0)
$applyRebootList.Children.Add($tb) | Out-Null
}
$applyRebootPanel.Visibility = 'Visible'
}
$applyWindow.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{})
}
catch {
-15
View File
@@ -89,19 +89,4 @@ Describe 'FeatureId parameter contracts' {
$missing | Should -HaveCount 0 -Because ($missing -join '; ')
}
It 'has an en-US Features translation with a Label for every FeatureId' {
$enUsPath = Join-Path $script:RepoRoot 'Config\Languages\en-US\Features.json'
$translations = (Get-Content -LiteralPath $enUsPath -Raw | ConvertFrom-Json).Features
$missing = @(
foreach ($feature in $script:Features) {
$entry = $translations.PSObject.Properties[$feature.FeatureId]
if ($null -eq $entry -or [string]::IsNullOrWhiteSpace($entry.Value.Label)) {
$feature.FeatureId
}
}
)
$missing | Should -HaveCount 0 -Because ($missing -join ', ')
}
}
+26
View File
@@ -4,6 +4,7 @@ BeforeAll {
function Test-AppInWingetList { param($appId, $InstalledList) $false }
function Invoke-NonBlocking { param($ScriptBlock, $ArgumentList, $TimeoutSeconds) }
function Get-UserName { 'Alice' }
function Get-Translation { param($Key, $FormatArgs) if ($FormatArgs) { return "$Key $($FormatArgs -join ' ')" }; return $Key }
function Invoke-ForceRemoveEdge {}
function Show-MessageBox { 'No' }
function Invoke-WithTargetUserHive { param($TargetUserName, $ScriptBlock, $ArgumentObject) }
@@ -20,6 +21,7 @@ Describe 'Remove-SelectedApps' {
$script:CancelRequested = $false
$script:ApplySubStepCallback = $null
$script:WingetInstalled = $true
$script:WingetDeferredRemovals = @{}
$script:AppRemovalFailures = 0
$script:AppRemovalVerificationUnavailable = $false
Mock Get-TargetUserForAppRemoval { 'AllUsers' }
@@ -54,6 +56,18 @@ Describe 'Remove-SelectedApps' {
Should -Invoke Test-AppInWingetList -Times 1 -Exactly -ParameterFilter { $appId -eq 'Winget.App' -and $InstalledList[0].Id -eq 'Other.App' }
}
It 'skips immediate inventory verification for a deferred WinGet uninstall' {
Mock Get-AppRemovalMethod { 'WinGet' }
Mock Remove-WinGetApp {
$script:WingetDeferredRemovals[$app] = 'Alice'
return $true
}
Remove-SelectedApps -appsList @('Winget.App') | Should -BeTrue
Should -Invoke Get-WingetInstalledApps -Times 0 -Exactly
}
It 'stops before the first removal when cancellation is requested' {
$script:CancelRequested = $true
Remove-SelectedApps -appsList @('One.App')
@@ -157,6 +171,7 @@ Describe 'Remove-WinGetApp' {
BeforeEach {
$script:Params = @{}
$script:WingetInstalled = $true
$script:WingetDeferredRemovals = @{}
Mock Invoke-NonBlocking { [PSCustomObject]@{ Success = $true; ExitCode = 0; Output = @() } }
Mock Set-RunOnceWingetTask { $true }
Mock Get-UserName { 'Alice' }
@@ -196,6 +211,17 @@ Describe 'Remove-WinGetApp' {
}
}
It 'defers a user-scope uninstall when WinGet returns the administrator-context error code' {
Mock Invoke-NonBlocking { [PSCustomObject]@{ ExitCode = -1978335107; Output = @('localized WinGet error') } }
Remove-WinGetApp -app 'One.App' | Should -BeTrue
Should -Invoke Set-RunOnceWingetTask -Times 1 -Exactly -ParameterFilter {
$appId -eq 'One.App'
}
$script:WingetDeferredRemovals['One.App'] | Should -Be 'Alice'
}
It 'reports a timed-out winget uninstall and continues' {
$script:Params = @{ User = 'Alice' }
Mock Invoke-NonBlocking { throw 'Operation timed out after 120 seconds' }
-5
View File
@@ -9,11 +9,6 @@ BeforeAll {
}
Describe 'Get-StartMenuUserNameFromPath' {
BeforeEach {
# Restore copies are named to the second; clear them so tests sharing $TestDrive cannot see each other's copy
Get-ChildItem -LiteralPath $TestDrive -Filter 'Win11Debloat-StartRestore-*.bak' | Remove-Item -Force
}
It 'extracts a user name from a start-menu path and falls back for unknown paths' {
Get-StartMenuUserNameFromPath -StartMenuBinFile 'C:\Users\Alice\AppData\Local\Packages\Start\start2.bin' | Should -Be 'Alice'
Get-StartMenuUserNameFromPath -StartMenuBinFile 'C:\Temp\start2.bin' | Should -Be 'unknown'