Compare commits

..

6 Commits

Author SHA1 Message Date
Jeffrey
9ee0126259 Bump version 2026-06-24 22:13:48 +02:00
Jeffrey
e23ecf36d6 Update minimum window sizing 2026-06-24 22:00:44 +02:00
Jeffrey
32dc3d6bdf Fix maximized window sizing (#673) 2026-06-24 21:45:53 +02:00
Jeffrey
f76adc5054 Add docstrings 2026-06-24 20:55:17 +02:00
Jeffrey
95b583606d Update start menu backup/restore with timestamped filenames (#672) 2026-06-24 17:32:45 +02:00
Jeffrey
693b805114 Simplify Window management (#671) 2026-06-24 14:40:48 +02:00
11 changed files with 298 additions and 261 deletions

View File

@@ -4,10 +4,10 @@ setlocal EnableDelayedExpansion
:: Set Windows Terminal installation paths. (Default and Scoop installation)
set "wtDefaultPath=%LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe"
set "wtScoopPath=%USERPROFILE%\scoop\apps\windows-terminal\current\wt.exe"
set "logFile=%LOCALAPPDATA%\Win11Debloat\Logs\Win11Debloat-Run.log"
set "logFile=%~dp0Logs\Win11Debloat-Run.log"
:: Ensure Logs folder exists
if not exist "%LOCALAPPDATA%\Win11Debloat\Logs" mkdir "%LOCALAPPDATA%\Win11Debloat\Logs"
if not exist "%~dp0Logs" mkdir "%~dp0Logs"
:: Determine which terminal exists
if exist "%wtDefaultPath%" (

View File

@@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat"
MinWidth="860" MinHeight="600"
MinWidth="860" MinHeight="640"
ResizeMode="CanResize"
SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
AllowsTransparency="False"
Background="{DynamicResource AppBorderColor}"
Foreground="{DynamicResource AppFgColor}">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome ResizeBorderThickness="5"
@@ -464,7 +464,7 @@
<Grid>
<StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top">
<!-- Logo -->
<Viewbox Width="250" Height="250" Margin="0,0,0,24" HorizontalAlignment="Center">
<Viewbox Width="250" Height="250" Margin="0,0,0,16" HorizontalAlignment="Center">
<Grid Width="250" Height="250">
<!-- Windows logo style icon -->
<Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z"
@@ -483,7 +483,7 @@
<!-- Title -->
<TextBlock Text="Welcome to Win11Debloat" FontSize="40" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,8,0,64">
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,4,0,48">
<Run Text="Your clean Windows experience is just a few clicks away!"/>
</TextBlock>

View File

@@ -113,16 +113,15 @@ function ReplaceStartMenu {
return
}
$startMenuBackupsDir = Join-Path $script:AppDataPath 'Backups'
if (-not (Test-Path $startMenuBackupsDir)) {
New-Item -ItemType Directory -Path $startMenuBackupsDir -Force | Out-Null
}
$backupTimestamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$backupBinFile = Join-Path $startMenuBackupsDir "Win11Debloat-Start2BinBackup-$userName-$backupTimestamp.bak"
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$backupFileName = "Win11Debloat-StartBackup-$timestamp.bak"
$startMenuDir = Split-Path $startMenuBinFile -Parent
$backupBinFile = Join-Path $startMenuDir $backupFileName
if (Test-Path $startMenuBinFile) {
# Backup current start menu file
Move-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
}
else {
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
@@ -194,6 +193,55 @@ function GetStartMenuUserNameFromPath {
}
<#
.SYNOPSIS
Returns the path to the latest start menu backup file for the given scope.
.DESCRIPTION
Resolves the LocalState folder for the specified scope and returns the
full path to the most recent Win11Debloat-StartBackup-*.bak file, or
$null if no backup exists.
For CurrentUser, uses $env:LOCALAPPDATA directly. For AllUsers, scans
every user profile.
.PARAMETER Scope
The scope to check: CurrentUser or AllUsers.
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'CurrentUser'
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'AllUsers'
#>
function Get-StartMenuBackupPath {
param(
[Parameter(Mandatory)]
[ValidateSet('CurrentUser', 'AllUsers')]
[string]$Scope
)
if ($Scope -eq 'CurrentUser') {
$localStateDir = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$latestBackup = Get-ChildItem -Path (Join-Path $localStateDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
return $null
}
else {
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
}
return $null
}
}
<#
.SYNOPSIS
@@ -209,14 +257,14 @@ function GetStartMenuUserNameFromPath {
The full path to the user's start2.bin file to restore.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, defaults to
StartMenuBinFile with a .bak extension.
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\start2.bin"
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuFromBackup {
param(
@@ -226,20 +274,32 @@ function RestoreStartMenuFromBackup {
)
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupTimestamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$startMenuBackupsDir = Join-Path $script:AppDataPath 'Backups'
if (-not (Test-Path $startMenuBackupsDir)) { New-Item -ItemType Directory -Path $startMenuBackupsDir -Force | Out-Null }
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
# Auto-detect latest backup in the same folder as the start2.bin
$startMenuDir = Split-Path $StartMenuBinFile -Parent
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
$resolvedBackupPath = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
Join-Path $startMenuBackupsDir "Win11Debloat-Start2BinBackup-$userName-$backupTimestamp.bak"
if ($latestBackup) { $latestBackup.FullName } else { $null }
}
else {
$BackupFilePath
}
$currentBinBackup = Join-Path $startMenuBackupsDir "Win11Debloat-Start2BinRestore-$userName-$backupTimestamp.bak"
$restoreTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$restoreBackupFileName = "Win11Debloat-StartRestore-$restoreTimestamp.bak"
$currentBinBackup = Join-Path (Split-Path $StartMenuBinFile -Parent) $restoreBackupFileName
if ([string]::IsNullOrWhiteSpace($backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore start menu for user $userName from backup $resolvedBackupPath" -ForegroundColor Cyan
Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan
return [PSCustomObject]@{
UserName = $userName
Result = $true
@@ -247,11 +307,11 @@ function RestoreStartMenuFromBackup {
}
}
if (-not (Test-Path -LiteralPath $resolvedBackupPath)) {
if (-not (Test-Path -LiteralPath $backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "Start menu backup file not found: $resolvedBackupPath"
Message = "No start menu backup file found for user $userName."
}
}
@@ -260,7 +320,7 @@ function RestoreStartMenuFromBackup {
Move-Item -Path $StartMenuBinFile -Destination $currentBinBackup -Force
}
Copy-Item -Path $resolvedBackupPath -Destination $StartMenuBinFile -Force
Copy-Item -Path $backupBinFile -Destination $StartMenuBinFile -Force
return [PSCustomObject]@{
UserName = $userName
Result = $true
@@ -281,34 +341,26 @@ function RestoreStartMenuFromBackup {
Restores the start menu for the current target user from a backup.
.DESCRIPTION
Resolves the start2.bin path for the current user (or the user specified
via the -User parameter), then delegates to RestoreStartMenuFromBackup.
Returns early with a warning if the user's start menu path cannot
be resolved.
Resolves the start2.bin path for the currently logged-in user, then
delegates to RestoreStartMenuFromBackup.
.PARAMETER BackupFilePath
Path to the backup file to restore from.
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenu -BackupFilePath "$env:LOCALAPPDATA\Win11Debloat\Backups\Win11Debloat-Start2BinBackup-Jeff-20260623_143000.bak"
RestoreStartMenu
.EXAMPLE
RestoreStartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenu {
param(
[Parameter(Mandatory)]
[string]$BackupFilePath
)
$targetUserName = GetUserName
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName
if ([string]::IsNullOrWhiteSpace($startMenuBinFile)) {
Write-Host "Unable to resolve start menu path for user $targetUserName, nothing to restore" -ForegroundColor Yellow
return [PSCustomObject]@{
UserName = $targetUserName
Result = $false
Message = "Could not resolve start menu path for user $targetUserName."
}
}
$targetUserName = $env:USERNAME
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Write-Host "Restoring start menu for user $targetUserName from backup..."
@@ -321,19 +373,24 @@ function RestoreStartMenu {
.DESCRIPTION
Iterates over every existing user profile and restores each user's
start2.bin from the specified backup file. For the Default user profile,
removes the start2.bin file (which was previously copied from a template)
so that new profiles revert to the system default start menu.
start2.bin from the latest backup in their LocalState folder. For the
Default user profile, removes the start2.bin file (which was previously
copied from a template) so that new profiles revert to the system
default start menu.
.PARAMETER BackupFilePath
Path to the backup file to restore from.
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak in each user's
LocalState folder.
.EXAMPLE
RestoreStartMenuForAllUsers -BackupFilePath "$env:LOCALAPPDATA\Win11Debloat\Backups\Win11Debloat-Start2BinBackup-Jeff-20260623_143000.bak"
RestoreStartMenuForAllUsers
.EXAMPLE
RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuForAllUsers {
function RestoreStartMenuForAllUsers {
param(
[Parameter(Mandatory)]
[string]$BackupFilePath
)

View File

@@ -1,71 +1,5 @@
# MainWindow-WindowChrome.ps1
# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations.
function Register-MaximizedWindowHelper {
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
}
# Window sizing, DPI-aware coordinate conversion, and UI animations.
# Convert screen-pixel coordinates to WPF device-independent pixels (DIP)
function ConvertTo-ScreenPointToDip {
@@ -118,16 +52,35 @@ function Update-MainWindowChrome {
)
$windowStateMaximized = [System.Windows.WindowState]::Maximized
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
if ($Window.WindowState -eq $windowStateMaximized) {
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
$resizeBorder = if ($chrome) { $chrome.ResizeBorderThickness } else { [System.Windows.SystemParameters]::WindowResizeBorderThickness }
# Compute margins using screen bounds vs working area
$marginLeft = $resizeBorder.Left
$marginTop = $resizeBorder.Top
$marginRight = $resizeBorder.Right
$marginBottom = $resizeBorder.Bottom
$screen = Get-WindowScreen -Window $Window
if ($screen) {
$workTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
$workSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
$screenTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.Bounds.Left -Y $screen.Bounds.Top
$screenSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.Bounds.Width -Height $screen.Bounds.Height
$marginLeft += ($workTL.X - $screenTL.X)
$marginTop += ($workTL.Y - $screenTL.Y)
$marginRight += ($screenTL.X + $screenSize.Width) - ($workTL.X + $workSize.Width)
$marginBottom += ($screenTL.Y + $screenSize.Height) - ($workTL.Y + $workSize.Height)
}
$MainBorder.Margin = [System.Windows.Thickness]::new($marginLeft, $marginTop, $marginRight, $marginBottom)
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(0)
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
$MainBorder.Effect = $null
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0)
# Zero out resize borders when maximized so the entire title bar row is draggable
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) }
}
else {
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
@@ -135,7 +88,6 @@ function Update-MainWindowChrome {
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
$MainBorder.Effect = $NormalWindowShadow
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0)
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) }
}
}

View File

@@ -1,3 +1,23 @@
<#
.SYNOPSIS
Hides the currently displayed bubble popup.
.DESCRIPTION
Closes the bubble popup with a smooth fade-out animation (220ms). If the
-Immediate switch is used, the popup is closed instantly without animation.
This function is called automatically by Show-Bubble's timer and can also
be invoked manually to dismiss the bubble early.
.PARAMETER Immediate
If specified, the bubble popup is closed instantly without a fade-out
animation. Any pending close timer is also stopped.
.EXAMPLE
Hide-Bubble
.EXAMPLE
Hide-Bubble -Immediate
#>
function Hide-Bubble {
param (
[Parameter(Mandatory=$false)]
@@ -37,6 +57,34 @@ function Hide-Bubble {
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut)
}
<#
.SYNOPSIS
Displays a transient bubble popup hint anchored above a target control.
.DESCRIPTION
Shows a WPF popup styled as a speech bubble above the specified target
control. The bubble fades in with a animation, displays for a configurable
duration, then fades out. Any previously shown bubble is dismissed
immediately before showing the new one.
.PARAMETER TargetControl
The WPF Control above which the bubble popup will be placed. This
parameter is mandatory.
.PARAMETER Message
The text message to display inside the bubble. Defaults to
'View the selected changes here'.
.PARAMETER DurationSeconds
The number of seconds the bubble remains visible before auto-hiding.
The minimum value is 1 second. Defaults to 5 seconds.
.EXAMPLE
Show-Bubble -TargetControl $myButton
.EXAMPLE
Show-Bubble -TargetControl $myButton -Message 'Changes saved!' -DurationSeconds 3
#>
function Show-Bubble {
param (
[Parameter(Mandatory=$true)]

View File

@@ -1,71 +1,6 @@
function Show-MainWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
# ---- Constrain maximized window to taskbar work area ----
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
$usesDarkMode = GetSystemUsesDarkMode
@@ -123,12 +58,6 @@
$window.Add_SourceInitialized({
& $applyInitialWindowSize
& $updateWindowChrome
$hwndHelper = New-Object System.Windows.Interop.WindowInteropHelper($window)
$hwndSource = [System.Windows.Interop.HwndSource]::FromHwnd($hwndHelper.Handle)
$hookMethod = [Win11Debloat.MaximizedWindowHelper].GetMethod('WmGetMinMaxInfoHook')
$hook = [System.Delegate]::CreateDelegate([System.Windows.Interop.HwndSourceHook], $hookMethod)
$hwndSource.AddHook($hook)
})
$window.Add_SizeChanged({
@@ -159,7 +88,7 @@
$menuReportBug.Add_Click({ Start-Process "https://github.com/Raphire/Win11Debloat/issues" })
$menuLogs.Add_Click({
$logsFolder = Split-Path $script:DefaultLogPath -Parent
$logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs'
if (Test-Path $logsFolder) {
Start-Process "explorer.exe" -ArgumentList $logsFolder
}

View File

@@ -61,7 +61,6 @@ function Show-RestoreBackupDialog {
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
$startMenuAutoBackupCheck.Visibility = 'Collapsed'
$introInfoPanel = $window.FindName('IntroInfoPanel')
$overviewPanel = $window.FindName('OverviewPanel')
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
@@ -149,8 +148,9 @@ function Show-RestoreBackupDialog {
return
}
$hasSelectedFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($hasSelectedFile) {
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
$primaryActionBtn.Content = 'Restore backup'
}
else {
@@ -196,6 +196,10 @@ function Show-RestoreBackupDialog {
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
# Show intro panel so user can configure scope & auto-detect
$startMenuAutoBackupCheck.IsChecked = $true
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
}
@@ -302,11 +306,15 @@ function Show-RestoreBackupDialog {
}
$handleStartMenuPrimaryAction = {
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$scope = (& $getStartMenuScopeInfo).Scope
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Start Menu Backup File'
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
$openDialog.InitialDirectory = (Join-Path $script:AppDataPath 'Backups')
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$openDialog.DefaultExt = '.bak'
if ($openDialog.ShowDialog($window) -ne $true) {
return
@@ -318,10 +326,21 @@ function Show-RestoreBackupDialog {
return
}
$scope = (& $getStartMenuScopeInfo).Scope
if (-not $useManualBackupFile) {
$scopeInfo = & $getStartMenuScopeInfo
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
if ($null -eq $autoBackupPath) {
$scopeText = $scopeInfo.SummaryText
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
return
}
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
}
$window.Tag = @{
Result = 'RestoreStartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
}
$window.DialogResult = $true
@@ -340,7 +359,16 @@ function Show-RestoreBackupDialog {
}
}
$startMenuAutoBackupCheck.Add_Checked({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Unchecked({
& $refreshStartMenuUi
})
$startMenuScopeCombo.Add_SelectionChanged({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
@@ -374,6 +402,7 @@ function Show-RestoreBackupDialog {
if ($state.WizardStep -eq 'StartMenu') {
$state.SelectedStartMenuBackupFilePath = $null
$startMenuAutoBackupCheck.IsChecked = $true
}
& $setWizardStep 'SelectType'

View File

@@ -40,6 +40,7 @@ function Show-RestoreBackupWindow {
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
$backupFilePath = $null
if ($dialogResult -is [hashtable] -and $dialogResult.ContainsKey('BackupFilePath')) {
$backupFilePath = $dialogResult['BackupFilePath']
@@ -48,7 +49,7 @@ function Show-RestoreBackupWindow {
$backupFilePath = $dialogResult.BackupFilePath
}
if ([string]::IsNullOrWhiteSpace($backupFilePath)) {
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($backupFilePath)) {
throw 'Start Menu restore canceled: no backup file selected.'
}

View File

@@ -131,33 +131,31 @@ catch {
Exit
}
# Migrate old user data from previous runs to AppData before cleanup
# Remove old script folder if it exists, but keep configs, logs and backups
if (Test-Path $tempWorkPath) {
$appDataPath = Join-Path $env:LOCALAPPDATA 'Win11Debloat'
if (-not (Test-Path $appDataPath)) { New-Item -ItemType Directory -Path $appDataPath -Force | Out-Null }
$oldBackupsDir = Join-Path $tempWorkPath 'Backups'
$oldLogsDir = Join-Path $tempWorkPath 'Logs'
$oldSettingsFile = Join-Path $tempWorkPath 'Config\LastUsedSettings.json'
if ((Test-Path $oldBackupsDir) -and (Get-ChildItem -Path $oldBackupsDir -ErrorAction SilentlyContinue)) {
$newBackupsDir = Join-Path $appDataPath 'Backups'
if (-not (Test-Path $newBackupsDir)) { New-Item -ItemType Directory -Path $newBackupsDir -Force | Out-Null }
Get-ChildItem -Path $oldBackupsDir | Move-Item -Destination $newBackupsDir -Force -ErrorAction SilentlyContinue
}
if ((Test-Path $oldLogsDir) -and (Get-ChildItem -Path $oldLogsDir -ErrorAction SilentlyContinue)) {
$newLogsDir = Join-Path $appDataPath 'Logs'
if (-not (Test-Path $newLogsDir)) { New-Item -ItemType Directory -Path $newLogsDir -Force | Out-Null }
Get-ChildItem -Path $oldLogsDir | Move-Item -Destination $newLogsDir -Force -ErrorAction SilentlyContinue
}
if (Test-Path $oldSettingsFile) {
Move-Item -Path $oldSettingsFile -Destination $appDataPath -Force -ErrorAction SilentlyContinue
}
Write-Output ""
Write-Output "> Cleaning up old Win11Debloat folder..."
Remove-Item $tempWorkPath -Recurse -Force
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
$configDir = Join-Path $tempWorkPath 'Config'
$backupDir = Join-Path $tempWorkPath 'ConfigOld'
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
if (Test-Path "$configDir") {
Write-Output ""
Write-Output "> Backing up existing config files..."
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @(
'LastUsedSettings.json'
)
Get-ChildItem -Path "$configDir" -Recurse | Where-Object { $_.Name -in $filesToKeep } | Move-Item -Destination "$backupDir"
Remove-Item "$configDir" -Recurse -Force
}
Write-Output ""
@@ -172,6 +170,19 @@ Remove-Item $tempArchivePath
# Move files
Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
# Add existing config files back to Config folder
if (Test-Path "$backupDir") {
if (-not (Test-Path "$configDir")) {
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
}
Write-Output ""
Write-Output "> Restoring existing config files..."
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
Remove-Item "$backupDir" -Recurse -Force
}
# Make list of arguments to pass on to the script
$arguments = $($PSBoundParameters.GetEnumerator() | ForEach-Object {
if ($_.Value -eq $true) {
@@ -208,12 +219,13 @@ if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit()
}
# Remove all remaining script files
# Remove all remaining script files, except for configs, logs and backups
if (Test-Path $tempWorkPath) {
Write-Output ""
Write-Output "> Cleaning up..."
Remove-Item $tempWorkPath -Recurse -Force
# Cleanup, remove Win11Debloat directory
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
Write-Output ""

View File

@@ -132,33 +132,31 @@ catch {
Exit
}
# Migrate old user data from previous runs to AppData before cleanup
# Remove old script folder if it exists, but keep configs, logs and backups
if (Test-Path $tempWorkPath) {
$appDataPath = Join-Path $env:LOCALAPPDATA 'Win11Debloat'
if (-not (Test-Path $appDataPath)) { New-Item -ItemType Directory -Path $appDataPath -Force | Out-Null }
$oldBackupsDir = Join-Path $tempWorkPath 'Backups'
$oldLogsDir = Join-Path $tempWorkPath 'Logs'
$oldSettingsFile = Join-Path $tempWorkPath 'Config\LastUsedSettings.json'
if ((Test-Path $oldBackupsDir) -and (Get-ChildItem -Path $oldBackupsDir -ErrorAction SilentlyContinue)) {
$newBackupsDir = Join-Path $appDataPath 'Backups'
if (-not (Test-Path $newBackupsDir)) { New-Item -ItemType Directory -Path $newBackupsDir -Force | Out-Null }
Get-ChildItem -Path $oldBackupsDir | Move-Item -Destination $newBackupsDir -Force -ErrorAction SilentlyContinue
}
if ((Test-Path $oldLogsDir) -and (Get-ChildItem -Path $oldLogsDir -ErrorAction SilentlyContinue)) {
$newLogsDir = Join-Path $appDataPath 'Logs'
if (-not (Test-Path $newLogsDir)) { New-Item -ItemType Directory -Path $newLogsDir -Force | Out-Null }
Get-ChildItem -Path $oldLogsDir | Move-Item -Destination $newLogsDir -Force -ErrorAction SilentlyContinue
}
if (Test-Path $oldSettingsFile) {
Move-Item -Path $oldSettingsFile -Destination $appDataPath -Force -ErrorAction SilentlyContinue
}
Write-Output ""
Write-Output "> Cleaning up old Win11Debloat folder..."
Remove-Item $tempWorkPath -Recurse -Force
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
$configDir = Join-Path $tempWorkPath 'Config'
$backupDir = Join-Path $tempWorkPath 'ConfigOld'
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
if (Test-Path "$configDir") {
Write-Output ""
Write-Output "> Backing up existing config files..."
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @(
'LastUsedSettings.json'
)
Get-ChildItem -Path "$configDir" -Recurse | Where-Object { $_.Name -in $filesToKeep } | Move-Item -Destination "$backupDir"
Remove-Item "$configDir" -Recurse -Force
}
Write-Output ""
@@ -173,6 +171,19 @@ Remove-Item $tempArchivePath
# Move files
Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
# Add existing config files back to Config folder
if (Test-Path "$backupDir") {
if (-not (Test-Path "$configDir")) {
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
}
Write-Output ""
Write-Output "> Restoring existing config files..."
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
Remove-Item "$backupDir" -Recurse -Force
}
# Make list of arguments to pass on to the script
$arguments = $($PSBoundParameters.GetEnumerator() | ForEach-Object {
if ($_.Value -eq $true) {
@@ -209,12 +220,13 @@ if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit()
}
# Remove all remaining script files
# Remove all remaining script files, except for configs, logs and backups
if (Test-Path $tempWorkPath) {
Write-Output ""
Write-Output "> Cleaning up..."
Remove-Item $tempWorkPath -Recurse -Force
# Cleanup, remove Win11Debloat directory
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
}
Write-Output ""

View File

@@ -137,19 +137,19 @@ if (-not $isAdmin) {
}
# Define script-level variables & paths
$script:Version = "2026.06.14"
$script:Version = "2026.06.24"
$configPath = Join-Path $PSScriptRoot 'Config'
$logsPath = Join-Path $PSScriptRoot 'Logs'
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
$scriptsPath = Join-Path $PSScriptRoot 'Scripts'
$script:AppDataPath = Join-Path $env:LOCALAPPDATA 'Win11Debloat'
$script:AppsListFilePath = Join-Path $configPath 'Apps.json'
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
$script:SavedSettingsFilePath = Join-Path $script:AppDataPath 'LastUsedSettings.json'
$script:DefaultLogPath = Join-Path (Join-Path $script:AppDataPath 'Logs') 'Win11Debloat.log'
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
$script:RegistryBackupsPath = Join-Path $script:AppDataPath 'Backups'
$script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
$script:AssetsPath = Join-Path $PSScriptRoot 'Assets'
$script:AppSelectionSchema = Join-Path $schemasPath 'AppSelectionWindow.xaml'
$script:MainWindowSchema = Join-Path $schemasPath 'MainWindow.xaml'
@@ -212,9 +212,6 @@ Write-Host ""
Write-Host ""
# Log script output to 'Win11Debloat.log' at the specified path
$logDir = if ($LogPath) { $LogPath } else { Split-Path $script:DefaultLogPath -Parent }
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
if ($LogPath -and (Test-Path $LogPath)) {
Start-Transcript -Path (Join-Path $LogPath 'Win11Debloat.log') -Append -IncludeInvocationHeader -Force | Out-Null
}