nodes found in autounattend.xml — setup scripts not pre-staged."
+ }
+ } catch {
+ & $Log "Warning: could not pre-stage setup scripts from autounattend.xml: $_"
+ }
+
+ if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
+ $isoDest = Join-Path $ISOContentsDir "autounattend.xml"
+ Set-Content -Path $isoDest -Value $AutoUnattendXml -Encoding UTF8 -Force
+ & $Log "Written autounattend.xml to ISO root ($isoDest)."
+ }
+ } else {
+ & $Log "Warning: autounattend.xml content is empty — skipping OOBE bypass file."
+ }
+
+ & $Log "Disabling reserved storage..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' 'ShippedWithReserves' 'REG_DWORD' '0'
+
+ & $Log "Disabling BitLocker device encryption..."
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Control\BitLocker' 'PreventDeviceEncryption' 'REG_DWORD' '1'
+
+ & $Log "Disabling Chat icon..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat' 'ChatIcon' 'REG_DWORD' '3'
+ Set-ISOScriptReg 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' 'TaskbarMn' 'REG_DWORD' '0'
+
+ & $Log "Disabling OneDrive folder backup..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\OneDrive' 'DisableFileSyncNGSC' 'REG_DWORD' '1'
+
+ & $Log "Disabling telemetry..."
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' 'Enabled' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\Privacy' 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' 'HasAccepted' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Input\TIPC' 'Enabled' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' 'RestrictImplicitInkCollection' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' 'RestrictImplicitTextCollection' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization\TrainedDataStore' 'HarvestContacts' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Personalization\Settings' 'AcceptedPrivacyPolicy' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Services\dmwappushservice' 'Start' 'REG_DWORD' '4'
+
+ & $Log "Preventing installation of DevHome and Outlook..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' 'workCompleted' 'REG_DWORD' '1'
+ Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate'
+ Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate'
+
+ & $Log "Disabling Copilot..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1'
+
+ & $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'NoAutoUpdate' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'AUOptions' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'UseWUServer' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'DisableWindowsUpdateAccess' 'REG_DWORD' '1'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUServer' 'REG_SZ' 'http://localhost:8080'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUStatusServer' 'REG_SZ' 'http://localhost:8080'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' 'workCompleted' 'REG_DWORD' '1'
+ Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate'
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' 'DODownloadMode' 'REG_DWORD' '0'
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Services\BITS' 'Start' 'REG_DWORD' '4'
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Services\wuauserv' 'Start' 'REG_DWORD' '4'
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Services\UsoSvc' 'Start' 'REG_DWORD' '4'
+ Set-ISOScriptReg 'HKLM\zSYSTEM\ControlSet001\Services\WaaSMedicSvc' 'Start' 'REG_DWORD' '4'
+
+ & $Log "Preventing installation of Teams..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Teams' 'DisableInstallation' 'REG_DWORD' '1'
+
+ & $Log "Preventing installation of new Outlook..."
+ Set-ISOScriptReg 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Mail' 'PreventRun' 'REG_DWORD' '1'
+
+ & $Log "Unloading offline registry hives..."
+ reg unload HKLM\zCOMPONENTS
+ reg unload HKLM\zDEFAULT
+ reg unload HKLM\zNTUSER
+ reg unload HKLM\zSOFTWARE
+ reg unload HKLM\zSYSTEM
+
+ # ── 5. Delete scheduled task definition files ─────────────────────────────
+ & $Log "Deleting scheduled task definition files..."
+ $tasksPath = "$ScratchDir\Windows\System32\Tasks"
+ Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\Chkdsk\Proxy" -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\Windows Error Reporting\QueueReporting" -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\InstallService" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\UpdateOrchestrator" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\UpdateAssistant" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
+ & $Log "Scheduled task files deleted."
+
+ # ── 6. Remove ISO support folder ─────────────────────────────────────────
+ if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
+ & $Log "Removing ISO support\ folder..."
+ Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force -ErrorAction SilentlyContinue
+ & $Log "ISO support\ folder removed."
+ }
+}
diff --git a/functions/private/Invoke-WinUtilISOUSB.ps1 b/functions/private/Invoke-WinUtilISOUSB.ps1
new file mode 100644
index 0000000000..4f1df01555
--- /dev/null
+++ b/functions/private/Invoke-WinUtilISOUSB.ps1
@@ -0,0 +1,268 @@
+function Invoke-WinUtilISORefreshUSBDrives {
+ $combo = $sync["WPFWin11ISOUSBDriveComboBox"]
+ $removable = @(Get-Disk | Where-Object { $_.BusType -eq "USB" } | Sort-Object Number)
+
+ $combo.Items.Clear()
+
+ if ($removable.Count -eq 0) {
+ $combo.Items.Add("No USB drives detected.")
+ $combo.SelectedIndex = 0
+ $sync["Win11ISOUSBDisks"] = @()
+ Write-Win11ISOLog "No USB drives detected."
+ return
+ }
+
+ foreach ($disk in $removable) {
+ $sizeGB = [math]::Round($disk.Size / 1GB, 1)
+ $combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
+ }
+ $combo.SelectedIndex = 0
+ Write-Win11ISOLog "Found $($removable.Count) USB drive(s)."
+ $sync["Win11ISOUSBDisks"] = $removable
+}
+
+function Invoke-WinUtilISOWriteUSB {
+ $contentsDir = $sync["Win11ISOContentsDir"]
+ $usbDisks = $sync["Win11ISOUSBDisks"]
+
+ if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
+ [System.Windows.MessageBox]::Show("No modified ISO content found. Please complete Steps 1-3 first.", "Not Ready", "OK", "Warning")
+ return
+ }
+
+ $combo = $sync["WPFWin11ISOUSBDriveComboBox"]
+ $selectedIndex = $combo.SelectedIndex
+ $selectedItemText = [string]$combo.SelectedItem
+ $usbDisks = @($usbDisks)
+
+ $targetDisk = $null
+ if ($selectedIndex -ge 0 -and $selectedIndex -lt $usbDisks.Count) {
+ $targetDisk = $usbDisks[$selectedIndex]
+ } elseif ($selectedItemText -match 'Disk\s+(\d+):') {
+ $selectedDiskNum = [int]$matches[1]
+ $targetDisk = $usbDisks | Where-Object { $_.Number -eq $selectedDiskNum } | Select-Object -First 1
+ }
+
+ if (-not $targetDisk) {
+ [System.Windows.MessageBox]::Show("Please select a USB drive from the dropdown.", "No Drive Selected", "OK", "Warning")
+ return
+ }
+
+ $diskNum = $targetDisk.Number
+ $sizeGB = [math]::Round($targetDisk.Size / 1GB, 1)
+
+ $confirm = [System.Windows.MessageBox]::Show(
+ "ALL data on Disk $diskNum ($($targetDisk.FriendlyName), $sizeGB GB) will be PERMANENTLY ERASED.`n`nAre you sure you want to continue?",
+ "Confirm USB Erase", "YesNo", "Warning")
+
+ if ($confirm -ne "Yes") {
+ Write-Win11ISOLog "USB write cancelled by user."
+ return
+ }
+
+ $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
+ Write-Win11ISOLog "Starting USB write to Disk $diskNum..."
+
+ $runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
+ $runspace.ApartmentState = "STA"
+ $runspace.ThreadOptions = "ReuseThread"
+ $runspace.Open()
+ $runspace.SessionStateProxy.SetVariable("sync", $sync)
+ $runspace.SessionStateProxy.SetVariable("diskNum", $diskNum)
+ $runspace.SessionStateProxy.SetVariable("contentsDir", $contentsDir)
+
+ $script = [Management.Automation.PowerShell]::Create()
+ $script.Runspace = $runspace
+ $script.AddScript({
+
+ function Log($msg) {
+ $ts = (Get-Date).ToString("HH:mm:ss")
+ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
+ $sync["WPFWin11ISOStatusLog"].Text += "`n[$ts] $msg"
+ $sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
+ $sync["WPFWin11ISOStatusLog"].ScrollToEnd()
+ })
+ }
+
+ function SetProgress($label, $pct) {
+ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
+ $sync.progressBarTextBlock.Text = $label
+ $sync.progressBarTextBlock.ToolTip = $label
+ $sync.ProgressBar.Value = [Math]::Max($pct, 5)
+ })
+ }
+
+ function Get-FreeDriveLetter {
+ $used = (Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue).Name
+ foreach ($c in [char[]](68..90)) {
+ if ($used -notcontains [string]$c) { return $c }
+ }
+ return $null
+ }
+
+ try {
+ SetProgress "Formatting USB drive..." 10
+
+ # Phase 1: Clean disk via diskpart (retry once if the drive is not yet ready)
+ $dpFile1 = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
+ "select disk $diskNum`nclean`nexit" | Set-Content -Path $dpFile1 -Encoding ASCII
+ Log "Running diskpart clean on Disk $diskNum..."
+ $dpCleanOut = diskpart /s $dpFile1 2>&1
+ $dpCleanOut | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
+ Remove-Item $dpFile1 -Force -ErrorAction SilentlyContinue
+
+ if (($dpCleanOut -join ' ') -match 'device is not ready') {
+ Log "Disk $diskNum was not ready; waiting 5 seconds and retrying clean..."
+ Start-Sleep -Seconds 5
+ Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
+ $dpFile1b = Join-Path $env:TEMP "winutil_diskpart_$(Get-Random).txt"
+ "select disk $diskNum`nclean`nexit" | Set-Content -Path $dpFile1b -Encoding ASCII
+ diskpart /s $dpFile1b 2>&1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
+ Remove-Item $dpFile1b -Force -ErrorAction SilentlyContinue
+ }
+
+ # Phase 2: Initialize as GPT
+ Start-Sleep -Seconds 2
+ Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
+ $diskObj = Get-Disk -Number $diskNum -ErrorAction Stop
+ if ($diskObj.PartitionStyle -eq 'RAW') {
+ Initialize-Disk -Number $diskNum -PartitionStyle GPT -ErrorAction Stop
+ Log "Disk $diskNum initialized as GPT."
+ } else {
+ Set-Disk -Number $diskNum -PartitionStyle GPT -ErrorAction Stop
+ Log "Disk $diskNum converted to GPT (was $($diskObj.PartitionStyle))."
+ }
+
+ # Phase 3: Create FAT32 partition via diskpart, then format with Format-Volume
+ # (diskpart's 'format' command can fail with "no volume selected" on fresh/never-formatted drives)
+ $volLabel = "W11-" + (Get-Date).ToString('yyMMdd')
+ $dpFile2 = Join-Path $env:TEMP "winutil_diskpart2_$(Get-Random).txt"
+ $maxFat32PartitionMB = 32768
+ $diskSizeMB = [int][Math]::Floor((Get-Disk -Number $diskNum -ErrorAction Stop).Size / 1MB)
+ $createPartitionCommand = "create partition primary"
+ if ($diskSizeMB -gt $maxFat32PartitionMB) {
+ $createPartitionCommand = "create partition primary size=$maxFat32PartitionMB"
+ Log "Disk $diskNum is $diskSizeMB MB; creating FAT32 partition capped at $maxFat32PartitionMB MB (32 GB)."
+ }
+
+ @(
+ "select disk $diskNum"
+ $createPartitionCommand
+ "exit"
+ ) | Set-Content -Path $dpFile2 -Encoding ASCII
+ Log "Creating partitions on Disk $diskNum..."
+ diskpart /s $dpFile2 2>&1 | Where-Object { $_ -match '\S' } | ForEach-Object { Log " diskpart: $_" }
+ Remove-Item $dpFile2 -Force -ErrorAction SilentlyContinue
+
+ SetProgress "Formatting USB partition..." 25
+ Start-Sleep -Seconds 3
+ Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
+
+ $partitions = Get-Partition -DiskNumber $diskNum -ErrorAction Stop
+ Log "Partitions on Disk $diskNum after creation: $($partitions.Count)"
+ foreach ($p in $partitions) {
+ Log " Partition $($p.PartitionNumber) Type=$($p.Type) Letter=$($p.DriveLetter) Size=$([math]::Round($p.Size/1MB))MB"
+ }
+
+ $winpePart = $partitions | Where-Object { $_.Type -eq "Basic" } | Select-Object -Last 1
+ if (-not $winpePart) {
+ throw "Could not find the Basic partition on Disk $diskNum after creation."
+ }
+
+ # Format using Format-Volume (reliable on fresh drives; diskpart format fails
+ # with 'no volume selected' when the partition has never been formatted before)
+ Log "Formatting Partition $($winpePart.PartitionNumber) as FAT32 (label: $volLabel)..."
+ Get-Partition -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber |
+ Format-Volume -FileSystem FAT32 -NewFileSystemLabel $volLabel -Force -Confirm:$false | Out-Null
+ Log "Partition $($winpePart.PartitionNumber) formatted as FAT32."
+
+ SetProgress "Assigning drive letters..." 30
+ Start-Sleep -Seconds 2
+ Update-Disk -Number $diskNum -ErrorAction SilentlyContinue
+
+ try { Remove-PartitionAccessPath -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -AccessPath "$($winpePart.DriveLetter):" -ErrorAction SilentlyContinue } catch {}
+ $usbLetter = Get-FreeDriveLetter
+ if (-not $usbLetter) { throw "No free drive letters (D-Z) available to assign to the USB data partition." }
+ Set-Partition -DiskNumber $diskNum -PartitionNumber $winpePart.PartitionNumber -NewDriveLetter $usbLetter
+ Log "Assigned drive letter $usbLetter to WINPE partition (Partition $($winpePart.PartitionNumber))."
+ Start-Sleep -Seconds 2
+
+ $usbDrive = "${usbLetter}:"
+ $retries = 0
+ while (-not (Test-Path $usbDrive) -and $retries -lt 6) {
+ $retries++
+ Log "Waiting for $usbDrive to become accessible (attempt $retries/6)..."
+ Start-Sleep -Seconds 2
+ }
+ if (-not (Test-Path $usbDrive)) { throw "Drive $usbDrive is not accessible after letter assignment." }
+ Log "USB data partition: $usbDrive"
+
+ $contentSizeBytes = (Get-ChildItem -LiteralPath $contentsDir -File -Recurse -Force -ErrorAction Stop | Measure-Object -Property Length -Sum).Sum
+ if (-not $contentSizeBytes) { $contentSizeBytes = 0 }
+ $usbVolume = Get-Volume -DriveLetter $usbLetter -ErrorAction Stop
+ $partitionCapacityBytes = [int64]$usbVolume.Size
+ $partitionFreeBytes = [int64]$usbVolume.SizeRemaining
+
+ $contentSizeGB = [math]::Round($contentSizeBytes / 1GB, 2)
+ $partitionCapacityGB = [math]::Round($partitionCapacityBytes / 1GB, 2)
+ $partitionFreeGB = [math]::Round($partitionFreeBytes / 1GB, 2)
+
+ Log "Source content size: $contentSizeGB GB. USB partition capacity: $partitionCapacityGB GB, free: $partitionFreeGB GB."
+
+ if ($contentSizeBytes -gt $partitionCapacityBytes) {
+ throw "ISO content ($contentSizeGB GB) is larger than the USB partition capacity ($partitionCapacityGB GB). Use a larger USB drive or reduce image size."
+ }
+
+ if ($contentSizeBytes -gt $partitionFreeBytes) {
+ throw "Insufficient free space on USB partition. Required: $contentSizeGB GB, available: $partitionFreeGB GB."
+ }
+
+ SetProgress "Copying Windows 11 files to USB..." 45
+
+ # Copy files; split install.wim if > 4 GB (FAT32 limit)
+ $installWim = Join-Path $contentsDir "sources\install.wim"
+ if (Test-Path $installWim) {
+ $wimSizeMB = [math]::Round((Get-Item $installWim).Length / 1MB)
+ if ($wimSizeMB -gt 3800) {
+ Log "install.wim is $wimSizeMB MB - splitting for FAT32 compatibility... This will take several minutes."
+ $splitDest = Join-Path $usbDrive "sources\install.swm"
+ New-Item -ItemType Directory -Path (Split-Path $splitDest) -Force | Out-Null
+ Split-WindowsImage -ImagePath $installWim -SplitImagePath $splitDest -FileSize 3800 -CheckIntegrity
+ Log "install.wim split complete."
+ Log "Copying remaining files to USB..."
+ & robocopy $contentsDir $usbDrive /E /XF install.wim /NFL /NDL /NJH /NJS
+ } else {
+ & robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
+ }
+ } else {
+ & robocopy $contentsDir $usbDrive /E /NFL /NDL /NJH /NJS
+ }
+
+ SetProgress "Finalising USB drive..." 90
+ Log "Files copied to USB."
+ SetProgress "USB write complete" 100
+ Log "USB drive is ready for use."
+
+ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show(
+ "USB drive created successfully!`n`nYou can now boot from this drive to install Windows 11.",
+ "USB Ready", "OK", "Info")
+ })
+ } catch {
+ Log "ERROR during USB write: $_"
+ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
+ [System.Windows.MessageBox]::Show("USB write failed:`n`n$_", "USB Write Error", "OK", "Error")
+ })
+ } finally {
+ Start-Sleep -Milliseconds 800
+ $sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
+ $sync.progressBarTextBlock.Text = ""
+ $sync.progressBarTextBlock.ToolTip = ""
+ $sync.ProgressBar.Value = 0
+ $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
+ })
+ }
+ }) | Out-Null
+
+ $script.BeginInvoke() | Out-Null
+}
diff --git a/functions/private/Invoke-WinUtilInstallPSProfile.ps1 b/functions/private/Invoke-WinUtilInstallPSProfile.ps1
index 551c935a71..f5eba446a3 100644
--- a/functions/private/Invoke-WinUtilInstallPSProfile.ps1
+++ b/functions/private/Invoke-WinUtilInstallPSProfile.ps1
@@ -1,103 +1,8 @@
function Invoke-WinUtilInstallPSProfile {
- <#
- .SYNOPSIS
- Backs up your original profile then installs and applies the CTT PowerShell profile.
- #>
- Invoke-WPFRunspace -ArgumentList $PROFILE -DebugPreference $DebugPreference -ScriptBlock {
- # Remap the automatic built-in $PROFILE variable to the parameter named $PSProfile.
- param ($PSProfile)
-
- function Invoke-PSSetup {
- # Define the URL used to download Chris Titus Tech's PowerShell profile.
- $url = "https://raw.githubusercontent.com/ChrisTitusTech/powershell-profile/main/Microsoft.PowerShell_profile.ps1"
-
- # Get the file hash for the user's current PowerShell profile.
- $OldHash = Get-FileHash $PSProfile -ErrorAction SilentlyContinue
-
- # Download Chris Titus Tech's PowerShell profile to the 'TEMP' folder.
- Invoke-RestMethod $url -OutFile "$env:TEMP/Microsoft.PowerShell_profile.ps1"
-
- # Get the file hash for Chris Titus Tech's PowerShell profile.
- $NewHash = Get-FileHash "$env:TEMP/Microsoft.PowerShell_profile.ps1"
-
- # Store the file hash of Chris Titus Tech's PowerShell profile.
- if (!(Test-Path "$PSProfile.hash")) {
- $NewHash.Hash | Out-File "$PSProfile.hash"
- }
-
- # Check if the new profile's hash doesn't match the old profile's hash.
- if ($NewHash.Hash -ne $OldHash.Hash) {
- # Check if oldprofile.ps1 exists and use it as a profile backup source.
- if (Test-Path "$env:USERPROFILE\oldprofile.ps1") {
- Write-Host "===> Backup File Exists... <===" -ForegroundColor Yellow
- Write-Host "===> Moving Backup File... <===" -ForegroundColor Yellow
- Copy-Item "$env:USERPROFILE\oldprofile.ps1" "$PSProfile.bak"
- Write-Host "===> Profile Backup: Done. <===" -ForegroundColor Yellow
- } else {
- # If oldprofile.ps1 does not exist use $PSProfile as a profile backup source.
- # Check if the profile backup file has not already been created on the disk.
- if ((Test-Path $PSProfile) -and (-not (Test-Path "$PSProfile.bak"))) {
- # Let the user know their PowerShell profile is being backed up.
- Write-Host "===> Backing Up Profile... <===" -ForegroundColor Yellow
-
- # Copy the user's current PowerShell profile to the backup file path.
- Copy-Item -Path $PSProfile -Destination "$PSProfile.bak"
-
- # Let the user know the profile backup has been completed successfully.
- Write-Host "===> Profile Backup: Done. <===" -ForegroundColor Yellow
- }
- }
-
- # Let the user know Chris Titus Tech's PowerShell profile is being installed.
- Write-Host "===> Installing Profile... <===" -ForegroundColor Yellow
-
- # Start a new hidden PowerShell instance because setup.ps1 does not work in runspaces.
- Start-Process -FilePath "pwsh" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -Command `"Invoke-Expression (Invoke-WebRequest `'https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1`')`"" -WindowStyle Hidden -Wait
-
- # Let the user know Chris Titus Tech's PowerShell profile has been installed successfully.
- Write-Host "Profile has been installed. Please restart your shell to reflect the changes!" -ForegroundColor Magenta
-
- # Let the user know Chris Titus Tech's PowerShell profile has been setup successfully.
- Write-Host "===> Finished Profile Setup <===" -ForegroundColor Yellow
- } else {
- # Let the user know Chris Titus Tech's PowerShell profile is already fully up-to-date.
- Write-Host "Profile is up to date" -ForegroundColor Magenta
- }
- }
-
- # Check if PowerShell Core is currently installed as a program and is available as a command.
- if (Get-Command "pwsh" -ErrorAction SilentlyContinue) {
- # Check if the version of PowerShell Core currently in use is version 7 or higher.
- if ($PSVersionTable.PSVersion.Major -ge 7) {
- # Invoke the PowerShell Profile setup script to install Chris Titus Tech's PowerShell Profile.
- Invoke-PSSetup
- } else {
- # Let the user know that PowerShell 7 is installed but is not currently in use.
- Write-Host "This profile requires Powershell 7, which is currently installed but not used!" -ForegroundColor Red
-
- # Load the necessary .NET library required to use Windows Forms to show dialog boxes.
- Add-Type -AssemblyName System.Windows.Forms
-
- # Display the message box asking if the user wants to install PowerShell 7 or not.
- $question = [System.Windows.Forms.MessageBox]::Show(
- "Profile requires Powershell 7, which is currently installed but not used! Do you want to install the profile for Powershell 7?",
- "Question",
- [System.Windows.Forms.MessageBoxButtons]::YesNo,
- [System.Windows.Forms.MessageBoxIcon]::Question
- )
-
- # Proceed with the installation and setup of the profile as the user pressed the 'Yes' button.
- if ($question -eq [System.Windows.Forms.DialogResult]::Yes) {
- Invoke-PSSetup
- } else {
- # Let the user know the setup of the profile will not proceed as they pressed the 'No' button.
- Write-Host "Not proceeding with the profile setup!" -ForegroundColor Magenta
- }
- }
- } else {
- # Let the user know that the profile requires PowerShell Core but it is not currently installed.
- Write-Host "This profile requires Powershell Core, which is currently not installed!" -ForegroundColor Red
- }
+ if (Test-Path $Profile) {
+ Rename-Item $Profile -NewName ($Profile + '.bak')
}
+
+ Start-Process pwsh -ArgumentList '-Command "irm https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1 | iex"'
}
diff --git a/functions/private/Invoke-WinUtilSSHServer.ps1 b/functions/private/Invoke-WinUtilSSHServer.ps1
index 7185af7ed3..08903f6c59 100644
--- a/functions/private/Invoke-WinUtilSSHServer.ps1
+++ b/functions/private/Invoke-WinUtilSSHServer.ps1
@@ -4,63 +4,29 @@ function Invoke-WinUtilSSHServer {
Enables OpenSSH server to remote into your windows device
#>
- # Get the latest version of OpenSSH Server
- $FeatureName = Get-WindowsCapability -Online | Where-Object { $_.Name -like "OpenSSH.Server*" }
-
# Install the OpenSSH Server feature if not already installed
- if ($FeatureName.State -ne "Installed") {
- Write-Host "Enabling OpenSSH Server"
- Add-WindowsCapability -Online -Name $FeatureName.Name
+ if ((Get-WindowsCapability -Name OpenSSH.Server -Online).State -ne "Installed") {
+ Write-Host "Enabling OpenSSH Server... This will take a long time"
+ Add-WindowsCapability -Name OpenSSH.Server -Online
}
- # Sets up the OpenSSH Server service
Write-Host "Starting the services"
- Start-Service -Name sshd
- Set-Service -Name sshd -StartupType Automatic
-
- # Sets up the ssh-agent service
- Start-Service 'ssh-agent'
- Set-Service -Name 'ssh-agent' -StartupType 'Automatic'
- # Confirm the required services are running
- $SSHDaemonService = Get-Service -Name sshd
- $SSHAgentService = Get-Service -Name 'ssh-agent'
+ Set-Service -Name sshd -StartupType Automatic
+ Start-Service -Name sshd
- if ($SSHDaemonService.Status -eq 'Running') {
- Write-Host "OpenSSH Server is running."
- } else {
- try {
- Write-Host "OpenSSH Server is not running. Attempting to restart..."
- Restart-Service -Name sshd -Force
- Write-Host "OpenSSH Server has been restarted successfully."
- } catch {
- Write-Host "Failed to restart OpenSSH Server: $_"
- }
- }
- if ($SSHAgentService.Status -eq 'Running') {
- Write-Host "ssh-agent is running."
- } else {
- try {
- Write-Host "ssh-agent is not running. Attempting to restart..."
- Restart-Service -Name sshd -Force
- Write-Host "ssh-agent has been restarted successfully."
- } catch {
- Write-Host "Failed to restart ssh-agent : $_"
- }
- }
+ Set-Service -Name ssh-agent -StartupType Automatic
+ Start-Service -Name ssh-agent
#Adding Firewall rule for port 22
Write-Host "Setting up firewall rules"
- $firewallRule = (Get-NetFirewallRule -Name 'sshd').Enabled
- if ($firewallRule) {
- Write-Host "Firewall rule for OpenSSH Server (sshd) already exists."
- } else {
+ if (-not ((Get-NetFirewallRule -Name 'sshd').Enabled)) {
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
Write-Host "Firewall rule for OpenSSH Server created and enabled."
}
# Check for the authorized_keys file
- $sshFolderPath = "$env:HOMEDRIVE\$env:HOMEPATH\.ssh"
+ $sshFolderPath = "$Home\.ssh"
$authorizedKeysPath = "$sshFolderPath\authorized_keys"
if (-not (Test-Path -Path $sshFolderPath)) {
@@ -72,10 +38,23 @@ function Invoke-WinUtilSSHServer {
Write-Host "Creating authorized_keys file..."
New-Item -Path $authorizedKeysPath -ItemType File -Force
Write-Host "authorized_keys file created at $authorizedKeysPath."
- } else {
- Write-Host "authorized_keys file already exists at $authorizedKeysPath."
}
+
+ Write-Host "Configuring sshd_config for standard authorized_keys behavior..."
+ $sshdConfigPath = "C:\ProgramData\ssh\sshd_config"
+
+ $configContent = Get-Content -Path $sshdConfigPath -Raw
+
+ $updatedContent = $configContent -replace '(?m)^(Match Group administrators)$', '# $1'
+ $updatedContent = $updatedContent -replace '(?m)^(\s+AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys)$', '# $1'
+
+ if ($updatedContent -ne $configContent) {
+ Set-Content -Path $sshdConfigPath -Value $updatedContent -Force
+ Write-Host "Commented out administrator-specific SSH key configuration in sshd_config"
+ Restart-Service -Name sshd -Force
+ }
+
Write-Host "OpenSSH server was successfully enabled."
- Write-Host "The config file can be located at C:\ProgramData\ssh\sshd_config "
+ Write-Host "The config file can be located at C:\ProgramData\ssh\sshd_config"
Write-Host "Add your public keys to this file -> $authorizedKeysPath"
}
diff --git a/functions/private/Invoke-WinUtilScript.ps1 b/functions/private/Invoke-WinUtilScript.ps1
index 05cef26a10..d49b9f4623 100644
--- a/functions/private/Invoke-WinUtilScript.ps1
+++ b/functions/private/Invoke-WinUtilScript.ps1
@@ -21,7 +21,7 @@ function Invoke-WinUtilScript {
)
try {
- Write-Host "Running Script for $name"
+ Write-Host "Running Script for $Name"
Invoke-Command $scriptblock -ErrorAction Stop
} catch [System.Management.Automation.CommandNotFoundException] {
Write-Warning "The specified command was not found."
@@ -37,7 +37,7 @@ function Invoke-WinUtilScript {
Write-Warning $PSItem.Exception.message
} catch {
# Generic catch block to handle any other type of exception
- Write-Warning "Unable to run script for $name due to unhandled exception"
+ Write-Warning "Unable to run script for $Name due to unhandled exception."
Write-Warning $psitem.Exception.StackTrace
}
diff --git a/functions/private/Invoke-WinUtilTweaks.ps1 b/functions/private/Invoke-WinUtilTweaks.ps1
index a682211157..2c8a669116 100644
--- a/functions/private/Invoke-WinUtilTweaks.ps1
+++ b/functions/private/Invoke-WinUtilTweaks.ps1
@@ -21,10 +21,6 @@ function Invoke-WinUtilTweaks {
$KeepServiceStartup = $true
)
- if ($Checkbox -contains "Toggle") {
- $CheckBox = $sync.configs.tweaks.$CheckBox
- }
-
Write-Debug "Tweaks: $($CheckBox)"
if($undo) {
$Values = @{
@@ -54,7 +50,7 @@ function Invoke-WinUtilTweaks {
$sync.configs.tweaks.$CheckBox.service | ForEach-Object {
$changeservice = $true
- # The check for !($undo) is required, without it the script will throw an error for accessing unavailable memeber, which's the 'OriginalService' Property
+ # The check for !($undo) is required, without it the script will throw an error for accessing unavailable member, which's the 'OriginalService' Property
if($KeepServiceStartup -AND !($undo)) {
try {
# Check if the service exists
@@ -64,7 +60,7 @@ function Invoke-WinUtilTweaks {
$changeservice = $false
}
} catch [System.ServiceProcess.ServiceNotFoundException] {
- Write-Warning "Service $($psitem.Name) was not found"
+ Write-Warning "Service $($psitem.Name) was not found."
}
}
@@ -80,9 +76,9 @@ function Invoke-WinUtilTweaks {
if (($psitem.Path -imatch "hku") -and !(Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
$null = (New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS)
if (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue) {
- Write-Debug "HKU drive created successfully"
+ Write-Debug "HKU drive created successfully."
} else {
- Write-Debug "Failed to create HKU drive"
+ Write-Debug "Failed to create HKU drive."
}
}
Set-WinUtilRegistry -Name $psitem.Name -Path $psitem.Path -Type $psitem.Type -Value $psitem.$($values.registry)
diff --git a/functions/private/Invoke-WinUtilUninstallPSProfile.ps1 b/functions/private/Invoke-WinUtilUninstallPSProfile.ps1
index 72272993ed..1a2addc252 100644
--- a/functions/private/Invoke-WinUtilUninstallPSProfile.ps1
+++ b/functions/private/Invoke-WinUtilUninstallPSProfile.ps1
@@ -1,231 +1,11 @@
function Invoke-WinUtilUninstallPSProfile {
- <#
- .SYNOPSIS
- # Uninstalls the CTT PowerShell profile then restores the original profile.
- #>
-
- Invoke-WPFRunspace -ArgumentList $PROFILE -DebugPreference $DebugPreference -ScriptBlock {
- # Remap the automatic built-in $PROFILE variable to the parameter named $PSProfile.
- param ($PSProfile)
-
- # Helper function used to uninstall a specific Nerd Fonts font package.
- function Uninstall-NerdFonts {
- # Define the parameters block for the Uninstall-NerdFonts function.
- param (
- [string]$FontsPath = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts",
- [string]$FontFamilyName = "CaskaydiaCoveNerdFont"
- )
-
- # Get the list of installed fonts as specified by the FontFamilyName parameter.
- $Fonts = Get-ChildItem $FontsPath -Recurse -Filter "*.ttf" | Where-Object { $_.Name -match $FontFamilyName }
-
- # Check if the specified fonts are currently installed on the system.
- if ($Fonts) {
- # Let the user know that the Nerd Fonts are currently being uninstalled.
- Write-Host "===> Uninstalling: Nerd Fonts... <===" -ForegroundColor Yellow
-
- # Loop over the font files and remove each installed font file one-by-one.
- $Fonts | ForEach-Object {
- # Check if the font file exists on the disk before attempting to remove it.
- if (Test-Path "$($_.FullName)") {
- # Remove the found font files from the disk; uninstalling the font.
- Remove-Item "$($_.FullName)"
- }
- }
- }
-
- # Let the user know that the Nerd Fonts package has been uninstalled from the system.
- if (-not $Fonts) {
- Write-Host "===> Successfully Uninstalled: Nerd Fonts. <===" -ForegroundColor Yellow
- }
-
- }
-
- # Helper function used to uninstall a specific Nerd Fonts font corresponding registry keys.
- function Uninstall-NerdFontRegKeys {
- # Define the parameters block for the Uninstall-NerdFontsRegKey function.
- param (
- [string]$FontsRegPath = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
- [string]$FontFamilyName = "CaskaydiaCove"
- )
-
- try {
- # Get all properties (font registrations) from the registry path
- $registryProperties = Get-ItemProperty -Path $FontsRegPath
-
- # Filter and remove properties that match the font family name
- $registryProperties.PSObject.Properties |
- Where-Object { $_.Name -match $FontFamilyName } |
- ForEach-Object {
- If ($_.Name -like "*$FontFamilyName*") {
- Remove-ItemProperty -path $FontsRegPath -Name $_.Name -ErrorAction SilentlyContinue
- }
- }
- } catch {
- Write-Host "Error removing registry keys: $($_.exception.message)" -ForegroundColor Red
- }
- }
-
- # Check if Chris Titus Tech's PowerShell profile is currently available in the PowerShell profile folder.
- if (Test-Path $PSProfile -PathType Leaf) {
- # Set the GitHub repo path used for looking up the name of Chris Titus Tech's powershell-profile repo.
- $GitHubRepoPath = "ChrisTitusTech/powershell-profile"
-
- # Get the unique identifier used to test for the presence of Chris Titus Tech's PowerShell profile.
- $PSProfileIdentifier = (Invoke-RestMethod "https://api.github.com/repos/$GitHubRepoPath").full_name
-
- # Check if Chris Titus Tech's PowerShell profile is currently installed in the PowerShell profile folder.
- if ((Get-Content $PSProfile) -match $PSProfileIdentifier) {
- # Attempt to uninstall Chris Titus Tech's PowerShell profile from the PowerShell profile folder.
- try {
- # Get the content of the backup PowerShell profile and store it in-memory.
- $PSProfileContent = Get-Content "$PSProfile.bak"
-
- # Store the flag used to check if OhMyPosh is in use by the backup PowerShell profile.
- $OhMyPoshInUse = $PSProfileContent -match "oh-my-posh init"
-
- # Check if OhMyPosh is not currently in use by the backup PowerShell profile.
- if (-not $OhMyPoshInUse) {
- # If OhMyPosh is currently installed attempt to uninstall it from the system.
- if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) {
- # Let the user know that OhMyPosh is currently being uninstalled from their system.
- Write-Host "===> Uninstalling: OhMyPosh... <===" -ForegroundColor Yellow
-
- # Attempt to uninstall OhMyPosh from the system with the WinGet package manager.
- winget uninstall -e --id JanDeDobbeleer.OhMyPosh
- }
- } else {
- # Let the user know that the uninstallation of OhMyPosh has been skipped because it is in use.
- Write-Host "===> Skipped Uninstall: OhMyPosh In-Use. <===" -ForegroundColor Yellow
- }
- } catch {
- # Let the user know that an error was encountered when uninstalling OhMyPosh.
- Write-Host "Failed to uninstall OhMyPosh. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to uninstall the specified Nerd Fonts package from the system.
- try {
- # Specify the directory that the specified font package will be uninstalled from.
- [string]$FontsPath = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts"
-
- # Specify the name of the font package that is to be uninstalled from the system.
- [string]$FontFamilyName = "CaskaydiaCoveNerdFont"
-
- # Call the function used to uninstall the specified Nerd Fonts package from the system.
- Uninstall-NerdFonts -FontsPath $FontsPath -FontFamilyName $FontFamilyName
-
- } catch {
- # Let the user know that an error was encountered when uninstalling Nerd Fonts.
- Write-Host "Failed to uninstall Nerd Fonts. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to uninstall the specified Nerd Fonts registry keys from the system.
- try {
- # Specify the registry path that the specified font registry keys will be uninstalled from.
- [string]$FontsRegPath = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
-
- # Specify the name of the font registry keys that is to be uninstalled from the system.
- [string]$FontFamilyName = "CaskaydiaCove"
-
- # Call the function used to uninstall the specified Nerd Fonts registry keys from the system.
- Uninstall-NerdFontRegKeys -FontsPath $FontsRegPath -FontFamilyName $FontFamilyName
-
- } catch {
- # Let the user know that an error was encountered when uninstalling Nerd Font registry keys.
- Write-Host "Failed to uninstall Nerd Font Registry Keys. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to uninstall the Terminal-Icons PowerShell module from the system.
- try {
- # Get the content of the backup PowerShell profile and store it in-memory.
- $PSProfileContent = Get-Content "$PSProfile.bak"
-
- # Store the flag used to check if Terminal-Icons is in use by the backup PowerShell profile.
- $TerminalIconsInUse = $PSProfileContent -match "Import-Module" -and $PSProfileContent -match "Terminal-Icons"
-
- # Check if Terminal-Icons is not currently in use by the backup PowerShell profile.
- if (-not $TerminalIconsInUse) {
- # If Terminal-Icons is currently installed attempt to uninstall it from the system.
- if (Get-Module -ListAvailable Terminal-Icons) {
- # Let the user know that Terminal-Icons is currently being uninstalled from their system.
- Write-Host "===> Uninstalling: Terminal-Icons... <===" -ForegroundColor Yellow
-
- # Attempt to uninstall Terminal-Icons from the system with Uninstall-Module.
- Uninstall-Module -Name Terminal-Icons
- }
- } else {
- # Let the user know that the uninstallation of Terminal-Icons has been skipped because it is in use.
- Write-Host "===> Skipped Uninstall: Terminal-Icons In-Use. <===" -ForegroundColor Yellow
- }
- } catch {
- # Let the user know that an error was encountered when uninstalling Terminal-Icons.
- Write-Host "Failed to uninstall Terminal-Icons. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to uninstall the Zoxide application from the system.
- try {
- # Get the content of the backup PowerShell profile and store it in-memory.
- $PSProfileContent = Get-Content "$PSProfile.bak"
-
- # Store the flag used to check if Zoxide is in use by the backup PowerShell profile.
- $ZoxideInUse = $PSProfileContent -match "zoxide init"
-
- # Check if Zoxide is not currently in use by the backup PowerShell profile.
- if (-not $ZoxideInUse) {
- # If Zoxide is currently installed attempt to uninstall it from the system.
- if (Get-Command zoxide -ErrorAction SilentlyContinue) {
- # Let the user know that Zoxide is currently being uninstalled from their system.
- Write-Host "===> Uninstalling: Zoxide... <===" -ForegroundColor Yellow
-
- # Attempt to uninstall Zoxide from the system with the WinGet package manager.
- winget uninstall -e --id ajeetdsouza.zoxide
- }
- } else {
- # Let the user know that the uninstallation of Zoxide been skipped because it is in use.
- Write-Host "===> Skipped Uninstall: Zoxide In-Use. <===" -ForegroundColor Yellow
- }
- } catch {
- # Let the user know that an error was encountered when uninstalling Zoxide.
- Write-Host "Failed to uninstall Zoxide. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to uninstall the CTT PowerShell profile from the system.
- try {
- # Try and remove the CTT PowerShell Profile file from the disk with Remove-Item.
- Remove-Item $PSProfile
-
- # Let the user know that the CTT PowerShell profile has been uninstalled from the system.
- Write-Host "Profile has been uninstalled. Please restart your shell to reflect the changes!" -ForegroundColor Magenta
- } catch {
- # Let the user know that an error was encountered when uninstalling the profile.
- Write-Host "Failed to uninstall profile. Error: $_" -ForegroundColor Red
- }
-
- # Attempt to move the user's original PowerShell profile backup back to its original location.
- try {
- # Check if the backup PowerShell profile exists before attempting to restore the backup.
- if (Test-Path "$PSProfile.bak") {
- # Restore the backup PowerShell profile and move it to its original location.
- Move-Item "$PSProfile.bak" $PSProfile
-
- # Let the user know that their PowerShell profile backup has been successfully restored.
- Write-Host "===> Restored Profile Backup. <===" -ForegroundColor Yellow
- }
- } catch {
- # Let the user know that an error was encountered when restoring the profile backup.
- Write-Host "Failed to restore profile backup. Error: $_" -ForegroundColor Red
- }
-
- # Silently cleanup the oldprofile.ps1 file that was created when the CTT PowerShell profile was installed.
- Remove-Item "$env:USERPROFILE\oldprofile.ps1" | Out-Null
- } else {
- # Let the user know that the CTT PowerShell profile is not installed and that the uninstallation was skipped.
- Write-Host "===> Chris Titus Tech's PowerShell Profile Not Found. Skipped Uninstallation. <===" -ForegroundColor Magenta
- }
- } else {
- # Let the user know that no PowerShell profile was found and that the uninstallation was skipped.
- Write-Host "===> No PowerShell Profile Found. Skipped Uninstallation. <===" -ForegroundColor Magenta
- }
+ if (Test-Path ($Profile + '.bak')) {
+ Remove-Item $Profile
+ Rename-Item ($Profile + '.bak') -NewName $Profile
+ }
+ else {
+ Remove-Item $Profile
}
-}
+ Write-Host "Successfully uninstalled CTT PowerShell Profile." -ForegroundColor Green
+}
diff --git a/functions/private/Invoke-WinutilThemeChange.ps1 b/functions/private/Invoke-WinutilThemeChange.ps1
index 0bf8f3fd7a..c85e5c9063 100644
--- a/functions/private/Invoke-WinutilThemeChange.ps1
+++ b/functions/private/Invoke-WinutilThemeChange.ps1
@@ -8,20 +8,14 @@ function Invoke-WinutilThemeChange {
modifying various UI elements such as colors, margins, corner radii, font families, etc.
If the '-init' switch is used, it initializes the theme based on the system's current dark mode setting.
- .PARAMETER init
- A switch parameter. If set to $true, the function initializes the theme based on the system’s current dark mode setting.
-
.EXAMPLE
Invoke-WinutilThemeChange
# Toggles the theme between 'Light' and 'Dark'.
- .EXAMPLE
- Invoke-WinutilThemeChange -init
- # Initializes the theme based on the system's dark mode and applies the shared theme.
+
#>
param (
- [switch]$init = $false,
- [string]$theme
+ [string]$theme = "Auto"
)
function Set-WinutilTheme {
@@ -129,51 +123,45 @@ function Invoke-WinutilThemeChange {
}
}
- $LightPreferencePath = "$env:LOCALAPPDATA\winutil\LightTheme.ini"
- $DarkPreferencePath = "$env:LOCALAPPDATA\winutil\DarkTheme.ini"
+ $sync.preferences.theme = $theme
+ Set-Preferences -save
+ Set-WinutilTheme -currentTheme "shared"
- if ($init) {
- Set-WinutilTheme -currentTheme "shared"
- if (Test-Path $LightPreferencePath) {
- $theme = "Light"
- }
- elseif (Test-Path $DarkPreferencePath) {
- $theme = "Dark"
- }
- else {
- $theme = "Auto"
- }
- }
-
- switch ($theme) {
+ switch ($sync.preferences.theme) {
"Auto" {
$systemUsesDarkMode = Get-WinUtilToggleStatus WPFToggleDarkMode
if ($systemUsesDarkMode) {
- Set-WinutilTheme -currentTheme "Dark"
+ $theme = "Dark"
}
else{
- Set-WinutilTheme -currentTheme "Light"
+ $theme = "Light"
}
-
+ Set-WinutilTheme -currentTheme $theme
$themeButtonIcon = [char]0xF08C
- Remove-Item $LightPreferencePath -Force -ErrorAction SilentlyContinue
- Remove-Item $DarkPreferencePath -Force -ErrorAction SilentlyContinue
}
"Dark" {
- Set-WinutilTheme -currentTheme $theme
+ Set-WinutilTheme -currentTheme $sync.preferences.theme
$themeButtonIcon = [char]0xE708
- $null = New-Item $DarkPreferencePath -Force
- Remove-Item $LightPreferencePath -Force -ErrorAction SilentlyContinue
}
"Light" {
- Set-WinutilTheme -currentTheme $theme
+ Set-WinutilTheme -currentTheme $sync.preferences.theme
$themeButtonIcon = [char]0xE706
- $null = New-Item $LightPreferencePath -Force
- Remove-Item $DarkPreferencePath -Force -ErrorAction SilentlyContinue
}
}
+ # Set FOSS Highlight Color
+ $fossEnabled = $true
+ if ($sync.WPFToggleFOSSHighlight) {
+ $fossEnabled = $sync.WPFToggleFOSSHighlight.IsChecked
+ }
+
+ if ($fossEnabled) {
+ $sync.Form.Resources["FOSSColor"] = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(76, 175, 80)) # #4CAF50
+ } else {
+ $sync.Form.Resources["FOSSColor"] = $sync.Form.Resources["MainForegroundColor"]
+ }
+
# Update the theme selector button with the appropriate icon
$ThemeButton = $sync.Form.FindName("ThemeButton")
$ThemeButton.Content = [string]$themeButtonIcon
diff --git a/functions/private/Remove-WinUtilAPPX.ps1 b/functions/private/Remove-WinUtilAPPX.ps1
index e90a101b3e..12c0ed4232 100644
--- a/functions/private/Remove-WinUtilAPPX.ps1
+++ b/functions/private/Remove-WinUtilAPPX.ps1
@@ -15,19 +15,7 @@ function Remove-WinUtilAPPX {
$Name
)
- try {
- Write-Host "Removing $Name"
- Get-AppxPackage "*$Name*" | Remove-AppxPackage -ErrorAction SilentlyContinue
- Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like "*$Name*" | Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
- } catch [System.Exception] {
- if ($psitem.Exception.Message -like "*The requested operation requires elevation*") {
- Write-Warning "Unable to uninstall $name due to a Security Exception"
- } else {
- Write-Warning "Unable to uninstall $name due to unhandled exception"
- Write-Warning $psitem.Exception.StackTrace
- }
- } catch {
- Write-Warning "Unable to uninstall $name due to unhandled exception"
- Write-Warning $psitem.Exception.StackTrace
- }
+ Write-Host "Removing $Name"
+ Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
+ Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
}
diff --git a/functions/private/Reset-WPFCheckBoxes.ps1 b/functions/private/Reset-WPFCheckBoxes.ps1
new file mode 100644
index 0000000000..59ae653318
--- /dev/null
+++ b/functions/private/Reset-WPFCheckBoxes.ps1
@@ -0,0 +1,77 @@
+function Reset-WPFCheckBoxes {
+ <#
+
+ .SYNOPSIS
+ Set winutil checkboxs to match $sync.selected values.
+ Should only need to be run if $sync.selected updated outside of UI (i.e. presets or import)
+
+ .PARAMETER doToggles
+ Whether or not to set UI toggles. WARNING: they will trigger if altered
+
+ .PARAMETER checkboxfilterpattern
+ The Pattern to use when filtering through CheckBoxes, defaults to "**"
+ Used to make reset blazingly fast.
+ #>
+
+ param (
+ [Parameter(position=0)]
+ [bool]$doToggles = $false,
+
+ [Parameter(position=1)]
+ [string]$checkboxfilterpattern = "**"
+ )
+
+ $CheckBoxesToCheck = $sync.selectedApps + $sync.selectedTweaks + $sync.selectedFeatures
+ $CheckBoxes = ($sync.GetEnumerator()).where{ $_.Value -is [System.Windows.Controls.CheckBox] -and $_.Name -notlike "WPFToggle*" -and $_.Name -like "$checkboxfilterpattern"}
+ Write-Debug "Getting checkboxes to set, number of checkboxes: $($CheckBoxes.Count)"
+
+ if ($CheckBoxesToCheck -ne "") {
+ $debugMsg = "CheckBoxes to Check are: "
+ $CheckBoxesToCheck | ForEach-Object { $debugMsg += "$_, " }
+ $debugMsg = $debugMsg -replace (',\s*$', '')
+ Write-Debug "$debugMsg"
+ }
+
+ foreach ($CheckBox in $CheckBoxes) {
+ $checkboxName = $CheckBox.Key
+ if (-not $CheckBoxesToCheck) {
+ $sync.$checkBoxName.IsChecked = $false
+ continue
+ }
+
+ # Check if the checkbox name exists in the flattened JSON hashtable
+ if ($CheckBoxesToCheck -contains $checkboxName) {
+ # If it exists, set IsChecked to true
+ $sync.$checkboxName.IsChecked = $true
+ Write-Debug "$checkboxName is checked"
+ } else {
+ # If it doesn't exist, set IsChecked to false
+ $sync.$checkboxName.IsChecked = $false
+ Write-Debug "$checkboxName is not checked"
+ }
+ }
+
+ # Update Installs tab UI values
+ $count = $sync.SelectedApps.Count
+ $sync.WPFselectedAppsButton.Content = "Selected Apps: $count"
+ # On every change, remove all entries inside the Popup Menu. This is done, so we can keep the alphabetical order even if elements are selected in a random way
+ $sync.selectedAppsstackPanel.Children.Clear()
+ $sync.selectedApps | Foreach-Object { Add-SelectedAppsMenuItem -name $($sync.configs.applicationsHashtable.$_.Content) -key $_ }
+
+ if($doToggles) {
+ # Restore toggle switch states from imported config.
+ # Only act on toggles that are explicitly listed in the import — toggles absent
+ # from the export file were not part of the saved config and should keep whatever
+ # state the live system already has (set during UI initialisation via Get-WinUtilToggleStatus).
+ $importedToggles = $sync.selectedToggles
+ $allToggles = $sync.GetEnumerator() | Where-Object { $_.Key -like "WPFToggle*" -and $_.Value -is [System.Windows.Controls.CheckBox] }
+ foreach ($toggle in $allToggles) {
+ if ($importedToggles -contains $toggle.Key) {
+ $sync[$toggle.Key].IsChecked = $true
+ Write-Debug "Restoring toggle: $($toggle.Key) = checked"
+ }
+ # Toggles not present in the import are intentionally left untouched;
+ # their current UI state already reflects the real system state.
+ }
+ }
+}
diff --git a/functions/private/Set-Preferences.ps1 b/functions/private/Set-Preferences.ps1
new file mode 100644
index 0000000000..56338c5302
--- /dev/null
+++ b/functions/private/Set-Preferences.ps1
@@ -0,0 +1,83 @@
+function Set-Preferences{
+
+ param(
+ [switch]$save=$false
+ )
+
+ # TODO delete this function sometime later
+ function Clean-OldPrefs{
+ if (Test-Path -Path "$winutildir\LightTheme.ini") {
+ $sync.preferences.theme = "Light"
+ Remove-Item -Path "$winutildir\LightTheme.ini"
+ }
+
+ if (Test-Path -Path "$winutildir\DarkTheme.ini") {
+ $sync.preferences.theme = "Dark"
+ Remove-Item -Path "$winutildir\DarkTheme.ini"
+ }
+
+ # check old prefs, if its first line has no =, then absorb it as pm
+ if (Test-Path -Path $iniPath) {
+ $oldPM = Get-Content $iniPath
+ if ($oldPM -notlike "*=*") {
+ $sync.preferences.packagemanager = $oldPM
+ }
+ }
+
+ if (Test-Path -Path "$winutildir\preferChocolatey.ini") {
+ $sync.preferences.packagemanager = "Choco"
+ Remove-Item -Path "$winutildir\preferChocolatey.ini"
+ }
+ }
+
+ function Save-Preferences{
+ $ini = ""
+ foreach($key in $sync.preferences.Keys) {
+ $pref = "$($key)=$($sync.preferences.$key)"
+ Write-Debug "Saving pref: $($pref)"
+ $ini = $ini + $pref + "`r`n"
+ }
+ $ini | Out-File $iniPath
+ }
+
+ function Load-Preferences{
+ Clean-OldPrefs
+ if (Test-Path -Path $iniPath) {
+ $iniData = Get-Content "$winutildir\preferences.ini"
+ foreach ($line in $iniData) {
+ if ($line -like "*=*") {
+ $arr = $line -split "=",-2
+ $key = $arr[0] -replace "\s",""
+ $value = $arr[1] -replace "\s",""
+ Write-Debug "Preference: Key = '$($key)' Value ='$($value)'"
+ $sync.preferences.$key = $value
+ }
+ }
+ }
+
+ # write defaults in case preferences dont exist
+ if ($null -eq $sync.preferences.theme) {
+ $sync.preferences.theme = "Auto"
+ }
+ if ($null -eq $sync.preferences.packagemanager) {
+ $sync.preferences.packagemanager = "Winget"
+ }
+
+ # convert packagemanager to enum
+ if ($sync.preferences.packagemanager -eq "Choco") {
+ $sync.preferences.packagemanager = [PackageManagers]::Choco
+ }
+ elseif ($sync.preferences.packagemanager -eq "Winget") {
+ $sync.preferences.packagemanager = [PackageManagers]::Winget
+ }
+ }
+
+ $iniPath = "$winutildir\preferences.ini"
+
+ if ($save) {
+ Save-Preferences
+ }
+ else {
+ Load-Preferences
+ }
+}
diff --git a/functions/private/Set-WinUtilDNS.ps1 b/functions/private/Set-WinUtilDNS.ps1
index 3aa4f157dd..04be90be45 100644
--- a/functions/private/Set-WinUtilDNS.ps1
+++ b/functions/private/Set-WinUtilDNS.ps1
@@ -15,7 +15,7 @@ function Set-WinUtilDNS {
if($DNSProvider -eq "Default") {return}
try {
$Adapters = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
- Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces"
+ Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces:"
Write-Host $($Adapters | Out-String)
Foreach ($Adapter in $Adapters) {
@@ -27,7 +27,7 @@ function Set-WinUtilDNS {
}
}
} catch {
- Write-Warning "Unable to set DNS Provider due to an unhandled exception"
+ Write-Warning "Unable to set DNS Provider due to an unhandled exception."
Write-Warning $psitem.Exception.StackTrace
}
}
diff --git a/functions/private/Set-WinUtilProgressbar.ps1 b/functions/private/Set-WinUtilProgressbar.ps1
index e63bd202db..2241e053c5 100644
--- a/functions/private/Set-WinUtilProgressbar.ps1
+++ b/functions/private/Set-WinUtilProgressbar.ps1
@@ -4,27 +4,25 @@ function Set-WinUtilProgressbar{
This function is used to Update the Progress Bar displayed in the winutil GUI.
It will be automatically hidden if the user clicks something and no process is running
.PARAMETER Label
- The Text to be overlayed onto the Progress Bar
+ The Text to be overlaid onto the Progress Bar
.PARAMETER PERCENT
The percentage of the Progress Bar that should be filled (0-100)
- .PARAMETER Hide
- If provided, the Progress Bar and the label will be hidden
#>
param(
[string]$Label,
[ValidateRange(0,100)]
- [int]$Percent,
- $Hide
+ [int]$Percent
)
- if ($hide) {
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBarLabel.Visibility = "Collapsed"})
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBar.Visibility = "Collapsed"})
- } else {
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBarLabel.Visibility = "Visible"})
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBar.Visibility = "Visible"})
+
+ if($PARAM_NOUI) {
+ return;
+ }
+
+ Invoke-WPFUIThread -ScriptBlock {$sync.progressBarTextBlock.Text = $label}
+ Invoke-WPFUIThread -ScriptBlock {$sync.progressBarTextBlock.ToolTip = $label}
+ if ($percent -lt 5 ) {
+ $percent = 5 # Ensure the progress bar is not empty, as it looks weird
}
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBarLabel.Content.Text = $label})
- $sync.form.Dispatcher.Invoke([action]{$sync.ProgressBarLabel.Content.ToolTip = $label})
- $sync.form.Dispatcher.Invoke([action]{ $sync.ProgressBar.Value = $percent})
+ Invoke-WPFUIThread -ScriptBlock { $sync.ProgressBar.Value = $percent}
}
diff --git a/functions/private/Set-WinUtilRegistry.ps1 b/functions/private/Set-WinUtilRegistry.ps1
index e99aa66972..3eacaf45e7 100644
--- a/functions/private/Set-WinUtilRegistry.ps1
+++ b/functions/private/Set-WinUtilRegistry.ps1
@@ -31,7 +31,7 @@ function Set-WinUtilRegistry {
if(!(Test-Path 'HKU:\')) {New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS}
If (!(Test-Path $Path)) {
- Write-Host "$Path was not found, Creating..."
+ Write-Host "$Path was not found. Creating..."
New-Item -Path $Path -Force -ErrorAction Stop | Out-Null
}
@@ -44,13 +44,13 @@ function Set-WinUtilRegistry {
Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction Stop | Out-Null
}
} catch [System.Security.SecurityException] {
- Write-Warning "Unable to set $Path\$Name to $Value due to a Security Exception"
+ Write-Warning "Unable to set $Path\$Name to $Value due to a Security Exception."
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Warning $psitem.Exception.ErrorRecord
} catch [System.UnauthorizedAccessException] {
Write-Warning $psitem.Exception.Message
} catch {
- Write-Warning "Unable to set $Name due to unhandled exception"
+ Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $psitem.Exception.StackTrace
}
}
diff --git a/functions/private/Set-WinUtilScheduledTask.ps1 b/functions/private/Set-WinUtilScheduledTask.ps1
index e6ee2bef9e..8cff7aa422 100644
--- a/functions/private/Set-WinUtilScheduledTask.ps1
+++ b/functions/private/Set-WinUtilScheduledTask.ps1
@@ -30,13 +30,13 @@ function Set-WinUtilScheduledTask {
}
} catch [System.Exception] {
if($psitem.Exception.Message -like "*The system cannot find the file specified*") {
- Write-Warning "Scheduled Task $name was not Found"
+ Write-Warning "Scheduled Task $Name was not found."
} else {
- Write-Warning "Unable to set $Name due to unhandled exception"
+ Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $psitem.Exception.Message
}
} catch {
- Write-Warning "Unable to run script for $name due to unhandled exception"
+ Write-Warning "Unable to run script for $name due to unhandled exception."
Write-Warning $psitem.Exception.StackTrace
}
}
diff --git a/functions/private/Set-WinUtilService.ps1 b/functions/private/Set-WinUtilService.ps1
index d2a7d45959..3ef8bd0630 100644
--- a/functions/private/Set-WinUtilService.ps1
+++ b/functions/private/Set-WinUtilService.ps1
@@ -24,12 +24,16 @@ Function Set-WinUtilService {
# Check if the service exists
$service = Get-Service -Name $Name -ErrorAction Stop
- # Service exists, proceed with changing properties
- $service | Set-Service -StartupType $StartupType -ErrorAction Stop
+ # Service exists, proceed with changing properties -- while handling auto delayed start for PWSH 5
+ if (($PSVersionTable.PSVersion.Major -lt 7) -and ($StartupType -eq "AutomaticDelayedStart")) {
+ sc.exe config $Name start=delayed-auto
+ } else {
+ $service | Set-Service -StartupType $StartupType -ErrorAction Stop
+ }
} catch [System.ServiceProcess.ServiceNotFoundException] {
- Write-Warning "Service $Name was not found"
+ Write-Warning "Service $Name was not found."
} catch {
- Write-Warning "Unable to set $Name due to unhandled exception"
+ Write-Warning "Unable to set $Name due to unhandled exception."
Write-Warning $_.Exception.Message
}
diff --git a/functions/private/Show-WPFInstallAppBusy.ps1 b/functions/private/Show-WPFInstallAppBusy.ps1
new file mode 100644
index 0000000000..94d0c0f9ba
--- /dev/null
+++ b/functions/private/Show-WPFInstallAppBusy.ps1
@@ -0,0 +1,21 @@
+function Show-WPFInstallAppBusy {
+ <#
+ .SYNOPSIS
+ Displays a busy overlay in the install app area of the WPF form.
+ This is used to indicate that an install or uninstall is in progress.
+ Dynamically updates the size of the overlay based on the app area on each invocation.
+ .PARAMETER text
+ The text to display in the busy overlay. Defaults to "Installing apps...".
+ #>
+ param (
+ $text = "Installing apps..."
+ )
+ Invoke-WPFUIThread -ScriptBlock {
+ $sync.InstallAppAreaOverlay.Visibility = [Windows.Visibility]::Visible
+ $sync.InstallAppAreaOverlay.Width = $($sync.InstallAppAreaScrollViewer.ActualWidth * 0.4)
+ $sync.InstallAppAreaOverlay.Height = $($sync.InstallAppAreaScrollViewer.ActualWidth * 0.4)
+ $sync.InstallAppAreaOverlayText.Text = $text
+ $sync.InstallAppAreaBorder.IsEnabled = $false
+ $sync.InstallAppAreaScrollViewer.Effect.Radius = 5
+ }
+}
diff --git a/functions/private/Test-WinUtilPackageManager.ps1 b/functions/private/Test-WinUtilPackageManager.ps1
index 04feb83c33..ed7546d85b 100644
--- a/functions/private/Test-WinUtilPackageManager.ps1
+++ b/functions/private/Test-WinUtilPackageManager.ps1
@@ -2,10 +2,10 @@ function Test-WinUtilPackageManager {
<#
.SYNOPSIS
- Checks if Winget and/or Choco are installed
+ Checks if WinGet and/or Choco are installed
.PARAMETER winget
- Check if Winget is installed
+ Check if WinGet is installed
.PARAMETER choco
Check if Chocolatey is installed
@@ -17,72 +17,25 @@ function Test-WinUtilPackageManager {
[System.Management.Automation.SwitchParameter]$choco
)
- $status = "not-installed"
-
if ($winget) {
- # Check if Winget is available while getting it's Version if it's available
- $wingetExists = $true
- try {
- $wingetVersionFull = winget --version
- } catch [System.Management.Automation.CommandNotFoundException], [System.Management.Automation.ApplicationFailedException] {
- Write-Warning "Winget was not found due to un-availablity reasons"
- $wingetExists = $false
- } catch {
- Write-Warning "Winget was not found due to un-known reasons, The Stack Trace is:`n$($psitem.Exception.StackTrace)"
- $wingetExists = $false
- }
-
- # If Winget is available, Parse it's Version and give proper information to Terminal Output.
- # If it isn't available, the return of this funtion will be "not-installed", indicating that
- # Winget isn't installed/available on The System.
- if ($wingetExists) {
- # Check if Preview Version
- if ($wingetVersionFull.Contains("-preview")) {
- $wingetVersion = $wingetVersionFull.Trim("-preview")
- $wingetPreview = $true
- } else {
- $wingetVersion = $wingetVersionFull
- $wingetPreview = $false
- }
-
- # Check if Winget's Version is too old.
- $wingetCurrentVersion = [System.Version]::Parse($wingetVersion.Trim('v'))
- # Grabs the latest release of Winget from the Github API for version check process.
- $response = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/Winget-cli/releases/latest" -Method Get -ErrorAction Stop
- $wingetLatestVersion = [System.Version]::Parse(($response.tag_name).Trim('v')) #Stores version number of latest release.
- $wingetOutdated = $wingetCurrentVersion -lt $wingetLatestVersion
+ if (Get-Command winget -ErrorAction SilentlyContinue) {
Write-Host "===========================================" -ForegroundColor Green
- Write-Host "--- Winget is installed ---" -ForegroundColor Green
+ Write-Host "--- WinGet is installed ---" -ForegroundColor Green
Write-Host "===========================================" -ForegroundColor Green
- Write-Host "Version: $wingetVersionFull" -ForegroundColor White
-
- if (!$wingetPreview) {
- Write-Host " - Winget is a release version." -ForegroundColor Green
- } else {
- Write-Host " - Winget is a preview version. Unexpected problems may occur." -ForegroundColor Yellow
- }
-
- if (!$wingetOutdated) {
- Write-Host " - Winget is Up to Date" -ForegroundColor Green
- $status = "installed"
- } else {
- Write-Host " - Winget is Out of Date" -ForegroundColor Red
- $status = "outdated"
- }
+ $status = "installed"
} else {
Write-Host "===========================================" -ForegroundColor Red
- Write-Host "--- Winget is not installed ---" -ForegroundColor Red
+ Write-Host "--- WinGet is not installed ---" -ForegroundColor Red
Write-Host "===========================================" -ForegroundColor Red
$status = "not-installed"
}
}
if ($choco) {
- if ((Get-Command -Name choco -ErrorAction Ignore) -and ($chocoVersion = (Get-Item "$env:ChocolateyInstall\choco.exe" -ErrorAction Ignore).VersionInfo.ProductVersion)) {
+ if (Get-Command choco -ErrorAction SilentlyContinue) {
Write-Host "===========================================" -ForegroundColor Green
Write-Host "--- Chocolatey is installed ---" -ForegroundColor Green
Write-Host "===========================================" -ForegroundColor Green
- Write-Host "Version: v$chocoVersion" -ForegroundColor White
$status = "installed"
} else {
Write-Host "===========================================" -ForegroundColor Red
diff --git a/functions/private/Uninstall-WinUtilEdgeBrowser.ps1 b/functions/private/Uninstall-WinUtilEdgeBrowser.ps1
deleted file mode 100644
index f090e95fa9..0000000000
--- a/functions/private/Uninstall-WinUtilEdgeBrowser.ps1
+++ /dev/null
@@ -1,138 +0,0 @@
-Function Uninstall-WinUtilEdgeBrowser {
- <#
- .SYNOPSIS
- Uninstall the Edge Browser (Chromium) from the system in an elegant way.
- .DESCRIPTION
- This will switch up the region to one of the EEA countries temporarily and uninstall the Edge Browser (Chromium).
- #>
-
- param (
- [Parameter(Mandatory = $true)]
- [ValidateSet("install", "uninstall")]
- [string]$action
- )
-
- function Uninstall-EdgeClient {
- param (
- [Parameter(Mandatory = $true)]
- [string]$Key
- )
-
- $originalNation = [microsoft.win32.registry]::GetValue('HKEY_USERS\.DEFAULT\Control Panel\International\Geo', 'Nation', [Microsoft.Win32.RegistryValueKind]::String)
-
- # Set Nation to any of the EEA regions temporarily
- # Refer: https://learn.microsoft.com/en-us/windows/win32/intl/table-of-geographical-locations
- $tmpNation = 68 # Ireland
- [microsoft.win32.registry]::SetValue('HKEY_USERS\.DEFAULT\Control Panel\International\Geo', 'Nation', $tmpNation, [Microsoft.Win32.RegistryValueKind]::String) | Out-Null
-
- $baseKey = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate'
- $registryPath = $baseKey + '\ClientState\' + $Key
-
- if (!(Test-Path -Path $registryPath)) {
- Write-Host "[$Mode] Registry key not found: $registryPath"
- return
- }
-
- # Remove the status flag
- Remove-ItemProperty -Path $baseKey -Name "IsEdgeStableUninstalled" -ErrorAction SilentlyContinue | Out-Null
-
- Remove-ItemProperty -Path $registryPath -Name "experiment_control_labels" -ErrorAction SilentlyContinue | Out-Null
-
- $uninstallString = (Get-ItemProperty -Path $registryPath).UninstallString
- $uninstallArguments = (Get-ItemProperty -Path $registryPath).UninstallArguments
-
- if ([string]::IsNullOrEmpty($uninstallString) -or [string]::IsNullOrEmpty($uninstallArguments)) {
- Write-Host "[$Mode] Cannot find uninstall methods for $Mode"
- return
- }
-
- # Extra arguments to nuke it
- $uninstallArguments += " --force-uninstall --delete-profile"
-
- # $uninstallCommand = "`"$uninstallString`"" + $uninstallArguments
- if (!(Test-Path -Path $uninstallString)) {
- Write-Host "[$Mode] setup.exe not found at: $uninstallString"
- return
- }
- Start-Process -FilePath $uninstallString -ArgumentList $uninstallArguments -Wait -NoNewWindow -Verbose
-
- # Restore Nation back to the original
- [microsoft.win32.registry]::SetValue('HKEY_USERS\.DEFAULT\Control Panel\International\Geo', 'Nation', $originalNation, [Microsoft.Win32.RegistryValueKind]::String) | Out-Null
-
- # might not exist in some cases
- if ((Get-ItemProperty -Path $baseKey).IsEdgeStableUninstalled -eq 1) {
- Write-Host "[$Mode] Edge Stable has been successfully uninstalled"
- }
- }
-
- function Uninstall-Edge {
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge" -Name "NoRemove" -ErrorAction SilentlyContinue | Out-Null
-
- [microsoft.win32.registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdateDev", "AllowUninstall", 1, [Microsoft.Win32.RegistryValueKind]::DWord) | Out-Null
-
- Uninstall-EdgeClient -Key '{56EB18F8-B008-4CBD-B6D2-8C97FE7E9062}'
-
- Remove-Item -Path "Computer\\HKEY_CLASSES_ROOT\\MSEdgePDF" -ErrorAction SilentlyContinue | Out-Null
- Remove-Item -Path "Computer\\HKEY_CLASSES_ROOT\\MSEdgeHTM" -ErrorAction SilentlyContinue | Out-Null
- Remove-Item -Path "Computer\\HKEY_CLASSES_ROOT\\MSEdgeMHT" -ErrorAction SilentlyContinue | Out-Null
-
- # Remove Edge Polocy reg keys
- Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" -Recurse -ErrorAction SilentlyContinue | Out-Null
-
- # Remove Edge reg keys
- Remove-Item -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Edge" -Recurse -ErrorAction SilentlyContinue | Out-Null
- }
-
- function Uninstall-WebView {
- # FIXME: might not work on some systems
-
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView" -Name "NoRemove" -ErrorAction SilentlyContinue | Out-Null
-
- Uninstall-EdgeClient -Key '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'
- }
-
- function Uninstall-EdgeUpdate {
- # FIXME: might not work on some systems
-
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge Update" -Name "NoRemove" -ErrorAction SilentlyContinue | Out-Null
-
- $registryPath = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate'
- if (!(Test-Path -Path $registryPath)) {
- Write-Host "Registry key not found: $registryPath"
- return
- }
- $uninstallCmdLine = (Get-ItemProperty -Path $registryPath).UninstallCmdLine
-
- if ([string]::IsNullOrEmpty($uninstallCmdLine)) {
- Write-Host "Cannot find uninstall methods for $Mode"
- return
- }
-
- Start-Process cmd.exe "/c $uninstallCmdLine" -WindowStyle Hidden -Wait
-
- # Remove EdgeUpdate reg keys
- Remove-Item -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" -Recurse -ErrorAction SilentlyContinue | Out-Null
- }
-
- function Install-Edge {
- $tempEdgePath = "$env:TEMP\MicrosoftEdgeSetup.exe"
-
- try {
- write-host "Installing Edge ..."
- Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2109047&Channel=Stable&language=en&consent=1" -OutFile $tempEdgePath
- Start-Process -FilePath $tempEdgePath -ArgumentList "/silent /install" -Wait
- Remove-item $tempEdgePath
- write-host "Edge Installed Successfully"
- } catch {
- write-host "Failed to install Edge"
- }
- }
-
- if ($action -eq "Install") {
- Install-Edge
- } elseif ($action -eq "Uninstall") {
- Uninstall-Edge
- Uninstall-EdgeUpdate
- # Uninstall-WebView - WebView is needed for Visual Studio and some MS Store Games like Forza
- }
-}
diff --git a/functions/private/Update-WinUtilProgramWinget.ps1 b/functions/private/Update-WinUtilProgramWinget.ps1
index 7f50d35e6c..4a40e22096 100644
--- a/functions/private/Update-WinUtilProgramWinget.ps1
+++ b/functions/private/Update-WinUtilProgramWinget.ps1
@@ -3,13 +3,13 @@ Function Update-WinUtilProgramWinget {
<#
.SYNOPSIS
- This will update all programs using Winget
+ This will update all programs using WinGet
#>
[ScriptBlock]$wingetinstall = {
- $host.ui.RawUI.WindowTitle = """Winget Install"""
+ $host.ui.RawUI.WindowTitle = """WinGet Install"""
Start-Transcript "$logdir\winget-update_$dateTime.log" -Append
winget upgrade --all --accept-source-agreements --accept-package-agreements --scope=machine --silent
diff --git a/functions/private/Update-WinUtilSelections.ps1 b/functions/private/Update-WinUtilSelections.ps1
new file mode 100644
index 0000000000..a4d65f0e61
--- /dev/null
+++ b/functions/private/Update-WinUtilSelections.ps1
@@ -0,0 +1,61 @@
+function Update-WinUtilSelections {
+ <#
+
+ .SYNOPSIS
+ Updates the $sync.selected variables with a given preset.
+
+ .PARAMETER flatJson
+ The flattened json list of $sync values to select.
+ #>
+
+ param (
+ $flatJson
+ )
+
+ Write-Debug "JSON to import: $($flatJson)"
+
+ foreach ($item in $flatJson) {
+ # Ensure each item is treated as a string to handle PSCustomObject from JSON deserialization
+ $cbkey = [string]$item
+ $group = if ($cbkey.StartsWith("WPFInstall")) { "Install" }
+ elseif ($cbkey.StartsWith("WPFTweaks")) { "Tweaks" }
+ elseif ($cbkey.StartsWith("WPFToggle")) { "Toggle" }
+ elseif ($cbkey.StartsWith("WPFFeature")) { "Feature" }
+ else { "na" }
+
+ switch ($group) {
+ "Install" {
+ if (!$sync.selectedApps.Contains($cbkey)) {
+ $sync.selectedApps.Add($cbkey)
+ # The List type needs to be specified again, because otherwise Sort-Object will convert the list to a string if there is only a single entry
+ [System.Collections.Generic.List[string]]$sync.selectedApps = $sync.SelectedApps | Sort-Object
+ }
+ }
+ "Tweaks" {
+ if (!$sync.selectedTweaks.Contains($cbkey)) {
+ $sync.selectedTweaks.Add($cbkey)
+ }
+ }
+ "Toggle" {
+ if (!$sync.selectedToggles.Contains($cbkey)) {
+ $sync.selectedToggles.Add($cbkey)
+ }
+ }
+ "Feature" {
+ if (!$sync.selectedFeatures.Contains($cbkey)) {
+ $sync.selectedFeatures.Add($cbkey)
+ }
+ }
+ default {
+ Write-Host "Unknown group for checkbox: $($cbkey)"
+ }
+ }
+ }
+
+ Write-Debug "-------------------------------------"
+ Write-Debug "Selected Apps: $($sync.selectedApps)"
+ Write-Debug "Selected Tweaks: $($sync.selectedTweaks)"
+ Write-Debug "Selected Toggles: $($sync.selectedToggles)"
+ Write-Debug "Selected Features: $($sync.selectedFeatures)"
+ Write-Debug "--------------------------------------"
+}
diff --git a/functions/public/Initialize-WPFUI.ps1 b/functions/public/Initialize-WPFUI.ps1
new file mode 100644
index 0000000000..46d8b9e893
--- /dev/null
+++ b/functions/public/Initialize-WPFUI.ps1
@@ -0,0 +1,120 @@
+function Initialize-WPFUI {
+ [OutputType([void])]
+ param(
+ [Parameter(Mandatory)]
+ [string]$TargetGridName
+ )
+
+ switch ($TargetGridName) {
+ "appscategory"{
+ # TODO
+ # Switch UI generation of the sidebar to this function
+ # $sync.ItemsControl = Initialize-InstallAppArea -TargetElement $TargetGridName
+ # ...
+
+ # Create and configure a popup for displaying selected apps
+ $selectedAppsPopup = New-Object Windows.Controls.Primitives.Popup
+ $selectedAppsPopup.IsOpen = $false
+ $selectedAppsPopup.PlacementTarget = $sync.WPFselectedAppsButton
+ $selectedAppsPopup.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom
+ $selectedAppsPopup.AllowsTransparency = $true
+
+ # Style the popup with a border and background
+ $selectedAppsBorder = New-Object Windows.Controls.Border
+ $selectedAppsBorder.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "MainBackgroundColor")
+ $selectedAppsBorder.SetResourceReference([Windows.Controls.Control]::BorderBrushProperty, "MainForegroundColor")
+ $selectedAppsBorder.SetResourceReference([Windows.Controls.Control]::BorderThicknessProperty, "ButtonBorderThickness")
+ $selectedAppsBorder.Width = 200
+ $selectedAppsBorder.Padding = 5
+ $selectedAppsPopup.Child = $selectedAppsBorder
+ $sync.selectedAppsPopup = $selectedAppsPopup
+
+ # Add a stack panel inside the popup's border to organize its child elements
+ $sync.selectedAppsstackPanel = New-Object Windows.Controls.StackPanel
+ $selectedAppsBorder.Child = $sync.selectedAppsstackPanel
+
+ # Close selectedAppsPopup when mouse leaves both button and selectedAppsPopup
+ $sync.WPFselectedAppsButton.Add_MouseLeave({
+ if (-not $sync.selectedAppsPopup.IsMouseOver) {
+ $sync.selectedAppsPopup.IsOpen = $false
+ }
+ })
+ $selectedAppsPopup.Add_MouseLeave({
+ if (-not $sync.WPFselectedAppsButton.IsMouseOver) {
+ $sync.selectedAppsPopup.IsOpen = $false
+ }
+ })
+
+ # Creates the popup that is displayed when the user right-clicks on an app entry
+ # This popup contains buttons for installing, uninstalling, and viewing app information
+
+ $appPopup = New-Object Windows.Controls.Primitives.Popup
+ $appPopup.StaysOpen = $false
+ $appPopup.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom
+ $appPopup.AllowsTransparency = $true
+ # Store the popup globally so the position can be set later
+ $sync.appPopup = $appPopup
+
+ $appPopupStackPanel = New-Object Windows.Controls.StackPanel
+ $appPopupStackPanel.Orientation = "Horizontal"
+ $appPopupStackPanel.Add_MouseLeave({
+ $sync.appPopup.IsOpen = $false
+ })
+ $appPopup.Child = $appPopupStackPanel
+
+ $appButtons = @(
+ [PSCustomObject]@{ Name = "Install"; Icon = [char]0xE118 },
+ [PSCustomObject]@{ Name = "Uninstall"; Icon = [char]0xE74D },
+ [PSCustomObject]@{ Name = "Info"; Icon = [char]0xE946 }
+ )
+ foreach ($button in $appButtons) {
+ $newButton = New-Object Windows.Controls.Button
+ $newButton.Style = $sync.Form.Resources.AppEntryButtonStyle
+ $newButton.Content = $button.Icon
+ $appPopupStackPanel.Children.Add($newButton) | Out-Null
+
+ # Dynamically load the selected app object so the buttons can be reused and do not need to be created for each app
+ switch ($button.Name) {
+ "Install" {
+ $newButton.Add_MouseEnter({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ $this.ToolTip = "Install or Upgrade $($appObject.content)"
+ })
+ $newButton.Add_Click({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ Invoke-WPFInstall -PackagesToInstall $appObject
+ })
+ }
+ "Uninstall" {
+ $newButton.Add_MouseEnter({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ $this.ToolTip = "Uninstall $($appObject.content)"
+ })
+ $newButton.Add_Click({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ Invoke-WPFUnInstall -PackagesToUninstall $appObject
+ })
+ }
+ "Info" {
+ $newButton.Add_MouseEnter({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ $this.ToolTip = "Open the application's website in your default browser`n$($appObject.link)"
+ })
+ $newButton.Add_Click({
+ $appObject = $sync.configs.applicationsHashtable.$($sync.appPopupSelectedApp)
+ Start-Process $appObject.link
+ })
+ }
+ }
+ }
+ }
+ "appspanel" {
+ $sync.ItemsControl = Initialize-InstallAppArea -TargetElement $TargetGridName
+ Initialize-InstallCategoryAppList -TargetElement $sync.ItemsControl -Apps $sync.configs.applicationsHashtable
+ }
+ default {
+ Write-Output "$TargetGridName not yet implemented"
+ }
+ }
+}
+
diff --git a/functions/public/Invoke-ScratchDialog.ps1 b/functions/public/Invoke-ScratchDialog.ps1
deleted file mode 100644
index 53ffc8fdc2..0000000000
--- a/functions/public/Invoke-ScratchDialog.ps1
+++ /dev/null
@@ -1,28 +0,0 @@
-
-function Invoke-ScratchDialog {
-
- <#
-
- .SYNOPSIS
- Enable Editable Text box Alternate Scartch path
-
- .PARAMETER Button
- #>
- $sync.WPFMicrowinISOScratchDir.IsChecked
-
-
- [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
- $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog
- $Dialog.SelectedPath = $sync.MicrowinScratchDirBox.Text
- $Dialog.ShowDialog()
- $filePath = $Dialog.SelectedPath
- Write-Host "No ISO is chosen+ $filePath"
-
- if ([string]::IsNullOrEmpty($filePath)) {
- Write-Host "No Folder had chosen"
- return
- }
-
- $sync.MicrowinScratchDirBox.Text = Join-Path $filePath "\"
-
-}
diff --git a/functions/public/Invoke-WPFButton.ps1 b/functions/public/Invoke-WPFButton.ps1
index 68cdfa28be..b0bc813a1f 100644
--- a/functions/public/Invoke-WPFButton.ps1
+++ b/functions/public/Invoke-WPFButton.ps1
@@ -15,50 +15,63 @@ function Invoke-WPFButton {
# Use this to get the name of the button
#[System.Windows.MessageBox]::Show("$Button","Chris Titus Tech's Windows Utility","OK","Info")
if (-not $sync.ProcessRunning) {
- Set-WinUtilProgressBar -label "" -percent 0 -hide $true
+ Set-WinUtilProgressBar -label "" -percent 0
}
+ # Check if button is defined in feature config with function or InvokeScript
+ if ($sync.configs.feature.$Button) {
+ $buttonConfig = $sync.configs.feature.$Button
+
+ # If button has a function defined, call it
+ if ($buttonConfig.function) {
+ $functionName = $buttonConfig.function
+ if (Get-Command $functionName -ErrorAction SilentlyContinue) {
+ & $functionName
+ return
+ }
+ }
+
+ # If button has InvokeScript defined, execute the scripts
+ if ($buttonConfig.InvokeScript -and $buttonConfig.InvokeScript.Count -gt 0) {
+ foreach ($script in $buttonConfig.InvokeScript) {
+ if (-not [string]::IsNullOrWhiteSpace($script)) {
+ Invoke-Expression $script
+ }
+ }
+ return
+ }
+ }
+
+ # Fallback to hard-coded switch for buttons not in feature.json
Switch -Wildcard ($Button) {
"WPFTab?BT" {Invoke-WPFTab $Button}
"WPFInstall" {Invoke-WPFInstall}
"WPFUninstall" {Invoke-WPFUnInstall}
"WPFInstallUpgrade" {Invoke-WPFInstallUpgrade}
+ "WPFCollapseAllCategories" {Invoke-WPFToggleAllCategories -Action "Collapse"}
+ "WPFExpandAllCategories" {Invoke-WPFToggleAllCategories -Action "Expand"}
"WPFStandard" {Invoke-WPFPresets "Standard" -checkboxfilterpattern "WPFTweak*"}
"WPFMinimal" {Invoke-WPFPresets "Minimal" -checkboxfilterpattern "WPFTweak*"}
"WPFClearTweaksSelection" {Invoke-WPFPresets -imported $true -checkboxfilterpattern "WPFTweak*"}
"WPFClearInstallSelection" {Invoke-WPFPresets -imported $true -checkboxfilterpattern "WPFInstall*"}
"WPFtweaksbutton" {Invoke-WPFtweaksbutton}
"WPFOOSUbutton" {Invoke-WPFOOSU}
- "WPFAddUltPerf" {Invoke-WPFUltimatePerformance -State "Enable"}
- "WPFRemoveUltPerf" {Invoke-WPFUltimatePerformance -State "Disable"}
+ "WPFAddUltPerf" {Invoke-WPFUltimatePerformance -Do}
+ "WPFRemoveUltPerf" {Invoke-WPFUltimatePerformance}
"WPFundoall" {Invoke-WPFundoall}
- "WPFFeatureInstall" {Invoke-WPFFeatureInstall}
- "WPFPanelDISM" {Invoke-WPFPanelDISM}
- "WPFPanelAutologin" {Invoke-WPFPanelAutologin}
- "WPFPanelcontrol" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelnetwork" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelpower" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelregion" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelsound" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelprinter" {Invoke-WPFControlPanel -Panel $button}
- "WPFPanelsystem" {Invoke-WPFControlPanel -Panel $button}
- "WPFPaneluser" {Invoke-WPFControlPanel -Panel $button}
- "WPFUpdatesdefault" {Invoke-WPFFixesUpdate}
- "WPFFixesUpdate" {Invoke-WPFFixesUpdate}
- "WPFFixesWinget" {Invoke-WPFFixesWinget}
- "WPFRunAdobeCCCleanerTool" {Invoke-WPFRunAdobeCCCleanerTool}
- "WPFFixesNetwork" {Invoke-WPFFixesNetwork}
+ "WPFUpdatesdefault" {Invoke-WPFUpdatesdefault}
"WPFUpdatesdisable" {Invoke-WPFUpdatesdisable}
"WPFUpdatessecurity" {Invoke-WPFUpdatessecurity}
- "WPFWinUtilShortcut" {Invoke-WPFShortcut -ShortcutToAdd "WinUtil" -RunAsAdmin $true}
"WPFGetInstalled" {Invoke-WPFGetInstalled -CheckBox "winget"}
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
- "WPFGetIso" {Invoke-MicrowinGetIso}
- "WPFMicrowin" {Invoke-Microwin}
- "WPFCloseButton" {Invoke-WPFCloseButton}
- "MicrowinScratchDirBT" {Invoke-ScratchDialog}
- "WPFWinUtilInstallPSProfile" {Invoke-WinUtilInstallPSProfile}
- "WPFWinUtilUninstallPSProfile" {Invoke-WinUtilUninstallPSProfile}
- "WPFWinUtilSSHServer" {Invoke-WPFSSHServer}
+ "WPFCloseButton" {$sync.Form.Close(); Write-Host "Bye bye!"}
+ "WPFselectedAppsButton" {$sync.selectedAppsPopup.IsOpen = -not $sync.selectedAppsPopup.IsOpen}
+ "WPFToggleFOSSHighlight" {
+ if ($sync.WPFToggleFOSSHighlight.IsChecked) {
+ $sync.Form.Resources["FOSSColor"] = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(76, 175, 80)) # #4CAF50
+ } else {
+ $sync.Form.Resources["FOSSColor"] = $sync.Form.Resources["MainForegroundColor"]
+ }
+ }
}
}
diff --git a/functions/public/Invoke-WPFCloseButton.ps1 b/functions/public/Invoke-WPFCloseButton.ps1
deleted file mode 100644
index 5ffff15aa7..0000000000
--- a/functions/public/Invoke-WPFCloseButton.ps1
+++ /dev/null
@@ -1,12 +0,0 @@
-function Invoke-WPFCloseButton {
-
- <#
-
- .SYNOPSIS
- Close application
-
- .PARAMETER Button
- #>
- $sync["Form"].Close()
- Write-Host "Bye bye!"
-}
diff --git a/functions/public/Invoke-WPFControlPanel.ps1 b/functions/public/Invoke-WPFControlPanel.ps1
deleted file mode 100644
index b7a628e7a8..0000000000
--- a/functions/public/Invoke-WPFControlPanel.ps1
+++ /dev/null
@@ -1,23 +0,0 @@
-function Invoke-WPFControlPanel {
- <#
-
- .SYNOPSIS
- Opens the requested legacy panel
-
- .PARAMETER Panel
- The panel to open
-
- #>
- param($Panel)
-
- switch ($Panel) {
- "WPFPanelcontrol" {cmd /c control}
- "WPFPanelnetwork" {cmd /c ncpa.cpl}
- "WPFPanelpower" {cmd /c powercfg.cpl}
- "WPFPanelregion" {cmd /c intl.cpl}
- "WPFPanelsound" {cmd /c mmsys.cpl}
- "WPFPanelprinter" {Start-Process "shell:::{A8A91A66-3A7D-4424-8D24-04E180695C7A}"}
- "WPFPanelsystem" {cmd /c sysdm.cpl}
- "WPFPaneluser" {cmd /c "control userpasswords2"}
- }
-}
diff --git a/functions/public/Invoke-WPFFeatureInstall.ps1 b/functions/public/Invoke-WPFFeatureInstall.ps1
index dd9995d254..958ba434ed 100644
--- a/functions/public/Invoke-WPFFeatureInstall.ps1
+++ b/functions/public/Invoke-WPFFeatureInstall.ps1
@@ -12,21 +12,25 @@ function Invoke-WPFFeatureInstall {
return
}
- $Features = (Get-WinUtilCheckBoxes)["WPFFeature"]
-
- Invoke-WPFRunspace -ArgumentList $Features -DebugPreference $DebugPreference -ScriptBlock {
- param($Features, $DebugPreference)
+ $handle = Invoke-WPFRunspace -ScriptBlock {
+ $Features = $sync.selectedFeatures
$sync.ProcessRunning = $true
if ($Features.count -eq 1) {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
} else {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
}
- Invoke-WinUtilFeatureInstall $Features
+ $x = 0
+
+ $Features | ForEach-Object {
+ Invoke-WinUtilFeatureInstall $_
+ $X++
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($x/$CheckBox.Count) }
+ }
$sync.ProcessRunning = $false
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "==================================="
Write-Host "--- Features are Installed ---"
diff --git a/functions/public/Invoke-WPFFixesNTPPool.ps1 b/functions/public/Invoke-WPFFixesNTPPool.ps1
new file mode 100644
index 0000000000..f858879f2d
--- /dev/null
+++ b/functions/public/Invoke-WPFFixesNTPPool.ps1
@@ -0,0 +1,20 @@
+function Invoke-WPFFixesNTPPool {
+ <#
+ .SYNOPSIS
+ Configures Windows to use pool.ntp.org for NTP synchronization
+
+ .DESCRIPTION
+ Replaces the default Windows NTP server (time.windows.com) with
+ pool.ntp.org for improved time synchronization accuracy and reliability.
+ #>
+
+ Start-Service w32time
+ w32tm /config /update /manualpeerlist:"pool.ntp.org,0x8" /syncfromflags:MANUAL
+
+ Restart-Service w32time
+ w32tm /resync
+
+ Write-Host "================================="
+ Write-Host "-- NTP Configuration Complete ---"
+ Write-Host "================================="
+}
diff --git a/functions/public/Invoke-WPFFixesNetwork.ps1 b/functions/public/Invoke-WPFFixesNetwork.ps1
index d741752829..5c3b4f87e4 100644
--- a/functions/public/Invoke-WPFFixesNetwork.ps1
+++ b/functions/public/Invoke-WPFFixesNetwork.ps1
@@ -8,13 +8,20 @@ function Invoke-WPFFixesNetwork {
Write-Host "Resetting Network with netsh"
+ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo"
# Reset WinSock catalog to a clean state
Start-Process -NoNewWindow -FilePath "netsh" -ArgumentList "winsock", "reset"
+
+ Set-WinUtilTaskbaritem -state "Normal" -value 0.35 -overlay "logo"
# Resets WinHTTP proxy setting to DIRECT
Start-Process -NoNewWindow -FilePath "netsh" -ArgumentList "winhttp", "reset", "proxy"
+
+ Set-WinUtilTaskbaritem -state "Normal" -value 0.7 -overlay "logo"
# Removes all user configured IP settings
Start-Process -NoNewWindow -FilePath "netsh" -ArgumentList "int", "ip", "reset"
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+
Write-Host "Process complete. Please reboot your computer."
$ButtonType = [System.Windows.MessageBoxButton]::OK
diff --git a/functions/public/Invoke-WPFFixesUpdate.ps1 b/functions/public/Invoke-WPFFixesUpdate.ps1
index 85beac60f3..4d7cfcefa4 100644
--- a/functions/public/Invoke-WPFFixesUpdate.ps1
+++ b/functions/public/Invoke-WPFFixesUpdate.ps1
@@ -6,19 +6,7 @@ function Invoke-WPFFixesUpdate {
Performs various tasks in an attempt to repair Windows Update
.DESCRIPTION
- 1. (Aggressive Only) Scans the system for corruption using chkdsk, SFC, and DISM
- Steps:
- 1. Runs chkdsk /scan /perf
- /scan - Runs an online scan on the volume
- /perf - Uses more system resources to complete a scan as fast as possible
- 2. Runs SFC /scannow
- /scannow - Scans integrity of all protected system files and repairs files with problems when possible
- 3. Runs DISM /Online /Cleanup-Image /RestoreHealth
- /Online - Targets the running operating system
- /Cleanup-Image - Performs cleanup and recovery operations on the image
- /RestoreHealth - Scans the image for component store corruption and attempts to repair the corruption using Windows Update
- 4. Runs SFC /scannow
- Ran twice in case DISM repaired SFC
+ 1. (Aggressive Only) Scans the system for corruption using the Invoke-WPFSystemRepair function
2. Stops Windows Update Services
3. Remove the QMGR Data file, which stores BITS jobs
4. (Aggressive Only) Renames the DataStore and CatRoot2 folders
@@ -42,108 +30,13 @@ function Invoke-WPFFixesUpdate {
param($Aggressive = $false)
Write-Progress -Id 0 -Activity "Repairing Windows Update" -PercentComplete 0
+ Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
+ Write-Host "Starting Windows Update Repair..."
# Wait for the first progress bar to show, otherwise the second one won't show
Start-Sleep -Milliseconds 200
if ($Aggressive) {
- # Scan system for corruption
- Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Scanning for corruption..." -PercentComplete 0
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running chkdsk..." -PercentComplete 0
- # 2>&1 redirects stdout, alowing iteration over the output
- chkdsk.exe /scan /perf 2>&1 | ForEach-Object {
- # Write stdout to the Verbose stream
- Write-Verbose $_
-
- # Get the index of the total percentage
- $index = $_.IndexOf("Total:")
- if (
- # If the percent is found
- ($percent = try {(
- $_.Substring(
- $index + 6,
- $_.IndexOf("%", $index) - $index - 6
- )
- ).Trim()} catch {0}) `
- <# And the current percentage is greater than the previous one #>`
- -and $percent -gt $oldpercent
- ) {
- # Update the progress bar
- $oldpercent = $percent
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running chkdsk... ($percent%)" -PercentComplete $percent
- }
- }
-
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running SFC..." -PercentComplete 0
- $oldpercent = 0
- # SFC has a bug when redirected which causes it to output only when the stdout buffer is full, causing the progress bar to move in chunks
- sfc /scannow 2>&1 | ForEach-Object {
- # Write stdout to the Verbose stream
- Write-Verbose $_
-
- # Filter for lines that contain a percentage that is greater than the previous one
- if (
- (
- # Use a different method to get the percentage that accounts for SFC's Unicode output
- [int]$percent = try {(
- (
- $_.Substring(
- $_.IndexOf("n") + 2,
- $_.IndexOf("%") - $_.IndexOf("n") - 2
- ).ToCharArray() | Where-Object {$_}
- ) -join ''
- ).TrimStart()} catch {0}
- ) -and $percent -gt $oldpercent
- ) {
- # Update the progress bar
- $oldpercent = $percent
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running SFC... ($percent%)" -PercentComplete $percent
- }
- }
-
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running DISM..." -PercentComplete 0
- $oldpercent = 0
- DISM /Online /Cleanup-Image /RestoreHealth | ForEach-Object {
- # Write stdout to the Verbose stream
- Write-Verbose $_
-
- # Filter for lines that contain a percentage that is greater than the previous one
- if (
- ($percent = try {
- [int]($_ -replace "\[" -replace "=" -replace " " -replace "%" -replace "\]")
- } catch {0}) `
- -and $percent -gt $oldpercent
- ) {
- # Update the progress bar
- $oldpercent = $percent
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running DISM... ($percent%)" -PercentComplete $percent
- }
- }
-
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running SFC again..." -PercentComplete 0
- $oldpercent = 0
- sfc /scannow 2>&1 | ForEach-Object {
- # Write stdout to the Verbose stream
- Write-Verbose $_
-
- # Filter for lines that contain a percentage that is greater than the previous one
- if (
- (
- [int]$percent = try {(
- (
- $_.Substring(
- $_.IndexOf("n") + 2,
- $_.IndexOf("%") - $_.IndexOf("n") - 2
- ).ToCharArray() | Where-Object {$_}
- ) -join ''
- ).TrimStart()} catch {0}
- ) -and $percent -gt $oldpercent
- ) {
- # Update the progress bar
- $oldpercent = $percent
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Running SFC... ($percent%)" -PercentComplete $percent
- }
- }
- Write-Progress -Id 1 -ParentId 0 -Activity "Scanning for corruption" -Status "Completed" -PercentComplete 100
+ Invoke-WPFSystemRepair
}
@@ -299,12 +192,15 @@ function Invoke-WPFFixesUpdate {
try {
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
} catch {
+ Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
Write-Warning "Failed to create Windows Update COM object: $_"
}
Start-Process -NoNewWindow -FilePath "wuauclt" -ArgumentList "/resetauthorization", "/detectnow"
Write-Progress -Id 10 -ParentId 0 -Activity "Forcing discovery" -Status "Completed" -PercentComplete 100
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Completed" -PercentComplete 100
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+
$ButtonType = [System.Windows.MessageBoxButton]::OK
$MessageboxTitle = "Reset Windows Update "
$Messageboxbody = ("Stock settings loaded.`n Please reboot your computer")
diff --git a/functions/public/Invoke-WPFFixesWinget.ps1 b/functions/public/Invoke-WPFFixesWinget.ps1
index f380c2ab4a..f41f371081 100644
--- a/functions/public/Invoke-WPFFixesWinget.ps1
+++ b/functions/public/Invoke-WPFFixesWinget.ps1
@@ -3,12 +3,21 @@ function Invoke-WPFFixesWinget {
<#
.SYNOPSIS
- Fixes Winget by running choco install winget
+ Fixes WinGet by running `choco install winget`
.DESCRIPTION
- BravoNorris for the fantastic idea of a button to reinstall winget
+ BravoNorris for the fantastic idea of a button to reinstall WinGet
#>
# Install Choco if not already present
- Install-WinUtilChoco
- Start-Process -FilePath "choco" -ArgumentList "install winget -y --force" -NoNewWindow -Wait
+ try {
+ Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
+ Write-Host "==> Starting WinGet Repair"
+ Install-WinUtilWinget
+ } catch {
+ Write-Error "Failed to install WinGet: $_"
+ Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
+ } finally {
+ Write-Host "==> Finished WinGet Repair"
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+ }
}
diff --git a/functions/public/Invoke-WPFFormVariables.ps1 b/functions/public/Invoke-WPFFormVariables.ps1
deleted file mode 100644
index 6a2d2e7ab3..0000000000
--- a/functions/public/Invoke-WPFFormVariables.ps1
+++ /dev/null
@@ -1,36 +0,0 @@
-Function Invoke-WPFFormVariables {
- <#
-
- .SYNOPSIS
- Prints the logo
-
- #>
- #If ($global:ReadmeDisplay -ne $true) { Write-Host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow; $global:ReadmeDisplay = $true }
-
-
- Write-Host ""
- Write-Host " CCCCCCCCCCCCCTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT "
- Write-Host " CCC::::::::::::CT:::::::::::::::::::::TT:::::::::::::::::::::T "
- Write-Host "CC:::::::::::::::CT:::::::::::::::::::::TT:::::::::::::::::::::T "
- Write-Host "C:::::CCCCCCCC::::CT:::::TT:::::::TT:::::TT:::::TT:::::::TT:::::T "
- Write-Host "C:::::C CCCCCCTTTTTT T:::::T TTTTTTTTTTTT T:::::T TTTTTT"
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C T:::::T T:::::T "
- Write-Host "C:::::C CCCCCC T:::::T T:::::T "
- Write-Host "C:::::CCCCCCCC::::C TT:::::::TT TT:::::::TT "
- Write-Host "CC:::::::::::::::C T:::::::::T T:::::::::T "
- Write-Host "CCC::::::::::::C T:::::::::T T:::::::::T "
- Write-Host " CCCCCCCCCCCCC TTTTTTTTTTT TTTTTTTTTTT "
- Write-Host ""
- Write-Host "====Chris Titus Tech====="
- Write-Host "=====Windows Toolbox====="
-
- #====DEBUG GUI Elements====
-
- #Write-Host "Found the following interactable elements from our form" -ForegroundColor Cyan
- #get-variable WPF*
-}
diff --git a/functions/public/Invoke-WPFGetInstalled.ps1 b/functions/public/Invoke-WPFGetInstalled.ps1
index c27fd13ce1..7b30a8cb78 100644
--- a/functions/public/Invoke-WPFGetInstalled.ps1
+++ b/functions/public/Invoke-WPFGetInstalled.ps1
@@ -9,44 +9,45 @@ function Invoke-WPFGetInstalled {
#>
param($checkbox)
-
- if($sync.ProcessRunning) {
+ if ($sync.ProcessRunning) {
$msg = "[Invoke-WPFGetInstalled] Install process is currently running."
[System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
- if(($sync.WPFpreferChocolatey.IsChecked -eq $false) -and ((Test-WinUtilPackageManager -winget) -eq "not-installed") -and $checkbox -eq "winget") {
+ if (($sync.ChocoRadioButton.IsChecked -eq $false) -and ((Test-WinUtilPackageManager -winget) -eq "not-installed") -and $checkbox -eq "winget") {
return
}
- $preferChoco = $sync.WPFpreferChocolatey.IsChecked
- Invoke-WPFRunspace -ArgumentList $checkbox, $preferChoco -DebugPreference $DebugPreference -ScriptBlock {
- param($checkbox, $preferChoco, $DebugPreference)
+ $managerPreference = $sync.preferences.packagemanager
+ Invoke-WPFRunspace -ParameterList @(("managerPreference", $managerPreference),("checkbox", $checkbox)) -ScriptBlock {
+ param (
+ [string]$checkbox,
+ [PackageManagers]$managerPreference
+ )
$sync.ProcessRunning = $true
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" }
- if($checkbox -eq "winget") {
+ if ($checkbox -eq "winget") {
Write-Host "Getting Installed Programs..."
+ switch ($managerPreference) {
+ "Choco"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox "choco"; break}
+ "Winget"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox; break}
+ }
}
- if($checkbox -eq "tweaks") {
+ elseif ($checkbox -eq "tweaks") {
Write-Host "Getting Installed Tweaks..."
- }
- if ($preferChoco -and $checkbox -eq "winget") {
- $Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox "choco"
- }
- else{
$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox
}
$sync.form.Dispatcher.invoke({
- foreach($checkbox in $Checkboxes) {
+ foreach ($checkbox in $Checkboxes) {
$sync.$checkbox.ischecked = $True
}
})
Write-Host "Done..."
$sync.ProcessRunning = $false
- $sync.form.Dispatcher.Invoke([action] { Set-WinUtilTaskbaritem -state "None" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" }
}
}
diff --git a/functions/public/Invoke-WPFImpex.ps1 b/functions/public/Invoke-WPFImpex.ps1
index 94bd9f1744..08a6138f4f 100644
--- a/functions/public/Invoke-WPFImpex.ps1
+++ b/functions/public/Invoke-WPFImpex.ps1
@@ -44,7 +44,14 @@ function Invoke-WPFImpex {
try {
$Config = ConfigDialog
if ($Config) {
- $jsonFile = Get-WinUtilCheckBoxes -unCheck $false | ConvertTo-Json
+ $allConfs = ($sync.selectedApps + $sync.selectedTweaks + $sync.selectedToggles + $sync.selectedFeatures) | ForEach-Object { [string]$_ }
+ if (-not $allConfs) {
+ [System.Windows.MessageBox]::Show(
+ "No settings are selected to export. Please select at least one app, tweak, toggle, or feature before exporting.",
+ "Nothing to Export", "OK", "Warning")
+ return
+ }
+ $jsonFile = $allConfs | ConvertTo-Json
$jsonFile | Out-File $Config -Force
"iex ""& { `$(irm https://christitus.com/win) } -Config '$Config'""" | Set-Clipboard
}
@@ -66,8 +73,36 @@ function Invoke-WPFImpex {
Write-Error "Failed to load the JSON file from the specified path or URL: $_"
return
}
- $flattenedJson = $jsonFile.PSObject.Properties.Where({ $_.Name -ne "Install" }).ForEach({ $_.Value })
- Invoke-WPFPresets -preset $flattenedJson -imported $true
+ # TODO how to handle old style? detected json type then flatten it in a func?
+ # $flattenedJson = $jsonFile.PSObject.Properties.Where({ $_.Name -ne "Install" }).ForEach({ $_.Value })
+ $flattenedJson = $jsonFile
+
+ if (-not $flattenedJson) {
+ [System.Windows.MessageBox]::Show(
+ "The selected file contains no settings to import. No changes have been made.",
+ "Empty Configuration", "OK", "Warning")
+ return
+ }
+
+ # Clear all existing selections before importing so the import replaces
+ # the current state rather than merging with it
+ $sync.selectedApps = [System.Collections.Generic.List[string]]::new()
+ $sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
+ $sync.selectedToggles = [System.Collections.Generic.List[string]]::new()
+ $sync.selectedFeatures = [System.Collections.Generic.List[string]]::new()
+
+ Update-WinUtilSelections -flatJson $flattenedJson
+
+ if (!$PARAM_NOUI) {
+ # Set flag so toggle Checked/Unchecked events don't trigger registry writes
+ # while we're programmatically restoring UI state from the imported config
+ $sync.ImportInProgress = $true
+ try {
+ Reset-WPFCheckBoxes -doToggles $true
+ } finally {
+ $sync.ImportInProgress = $false
+ }
+ }
}
} catch {
Write-Error "An error occurred while importing: $_"
diff --git a/functions/public/Invoke-WPFInstall.ps1 b/functions/public/Invoke-WPFInstall.ps1
index ef03af36da..3d052e3115 100644
--- a/functions/public/Invoke-WPFInstall.ps1
+++ b/functions/public/Invoke-WPFInstall.ps1
@@ -1,80 +1,55 @@
function Invoke-WPFInstall {
<#
-
.SYNOPSIS
Installs the selected programs using winget, if one or more of the selected programs are already installed on the system, winget will try and perform an upgrade if there's a newer version to install.
-
#>
+ $PackagesToInstall = $sync.selectedApps | Foreach-Object { $sync.configs.applicationsHashtable.$_ }
+
+
if($sync.ProcessRunning) {
$msg = "[Invoke-WPFInstall] An Install process is currently running."
[System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
- $PackagesToInstall = (Get-WinUtilCheckBoxes)["Install"]
- Write-Host $PackagesToInstall
if ($PackagesToInstall.Count -eq 0) {
- $WarningMsg = "Please select the program(s) to install or upgrade"
+ $WarningMsg = "Please select the program(s) to install or upgrade."
[System.Windows.MessageBox]::Show($WarningMsg, $AppTitle, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
}
- $ChocoPreference = $($sync.WPFpreferChocolatey.IsChecked)
- $installHandle = Invoke-WPFRunspace -ParameterList @(("PackagesToInstall", $PackagesToInstall),("ChocoPreference", $ChocoPreference)) -DebugPreference $DebugPreference -ScriptBlock {
- param($PackagesToInstall, $ChocoPreference, $DebugPreference)
- if ($PackagesToInstall.count -eq 1) {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" })
- } else {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" })
- }
- $packagesWinget, $packagesChoco = {
- $packagesWinget = [System.Collections.ArrayList]::new()
- $packagesChoco = [System.Collections.ArrayList]::new()
- foreach ($package in $PackagesToInstall) {
- if ($ChocoPreference) {
- if ($package.choco -eq "na") {
- $packagesWinget.add($package.winget)
- Write-Host "Queueing $($package.winget) for Winget install"
- } else {
- $null = $packagesChoco.add($package.choco)
- Write-Host "Queueing $($package.choco) for Chocolatey install"
- }
- }
- else {
- if ($package.winget -eq "na") {
- $packagesChoco.add($package.choco)
- Write-Host "Queueing $($package.choco) for Chocolatey install"
- } else {
- $null = $packagesWinget.add($($package.winget))
- Write-Host "Queueing $($package.winget) for Winget install"
- }
- }
- }
- return $packagesWinget, $packagesChoco
- }.Invoke($PackagesToInstall)
+ $ManagerPreference = $sync.preferences.packagemanager
+
+ $handle = Invoke-WPFRunspace -ParameterList @(("PackagesToInstall", $PackagesToInstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
+ param($PackagesToInstall, $ManagerPreference)
+
+ $packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToInstall -Preference $ManagerPreference
+
+ $packagesWinget = $packagesSorted[[PackageManagers]::Winget]
+ $packagesChoco = $packagesSorted[[PackageManagers]::Choco]
try {
$sync.ProcessRunning = $true
- $errorPackages = @()
- if($packagesWinget.Count -gt 0) {
+ if($packagesWinget.Count -gt 0 -and $packagesWinget -ne "0") {
+ Show-WPFInstallAppBusy -text "Installing apps..."
Install-WinUtilWinget
Install-WinUtilProgramWinget -Action Install -Programs $packagesWinget
-
}
if($packagesChoco.Count -gt 0) {
Install-WinUtilChoco
Install-WinUtilProgramChoco -Action Install -Programs $packagesChoco
}
+ Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "-- Installs have finished ---"
Write-Host "==========================================="
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
} catch {
Write-Host "==========================================="
Write-Host "Error: $_"
Write-Host "==========================================="
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -overlay "warning" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
$sync.ProcessRunning = $False
}
diff --git a/functions/public/Invoke-WPFInstallUpgrade.ps1 b/functions/public/Invoke-WPFInstallUpgrade.ps1
index c86ee12327..0f262a0d64 100644
--- a/functions/public/Invoke-WPFInstallUpgrade.ps1
+++ b/functions/public/Invoke-WPFInstallUpgrade.ps1
@@ -5,14 +5,14 @@ function Invoke-WPFInstallUpgrade {
Invokes the function that upgrades all installed programs
#>
- if ($sync.WPFpreferChocolatey.IsChecked) {
+ if ($sync.ChocoRadioButton.IsChecked) {
Install-WinUtilChoco
$chocoUpgradeStatus = (Start-Process "choco" -ArgumentList "upgrade all -y" -Wait -PassThru -NoNewWindow).ExitCode
if ($chocoUpgradeStatus -eq 0) {
Write-Host "Upgrade Successful"
}
else{
- Write-Host "Error Occured. Return Code: $chocoUpgradeStatus"
+ Write-Host "Error Occurred. Return Code: $chocoUpgradeStatus"
}
}
else{
diff --git a/functions/public/Invoke-WPFPanelDISM.ps1 b/functions/public/Invoke-WPFPanelDISM.ps1
deleted file mode 100644
index 31b36364d6..0000000000
--- a/functions/public/Invoke-WPFPanelDISM.ps1
+++ /dev/null
@@ -1,30 +0,0 @@
-function Invoke-WPFPanelDISM {
- <#
-
- .SYNOPSIS
- Checks for system corruption using Chkdsk, SFC, and DISM
-
- .DESCRIPTION
- 1. Chkdsk - Fixes disk and filesystem corruption
- 2. SFC Run 1 - Fixes system file corruption, and fixes DISM if it was corrupted
- 3. DISM - Fixes system image corruption, and fixes SFC's system image if it was corrupted
- 4. SFC Run 2 - Fixes system file corruption, this time with an almost guaranteed uncorrupted system image
-
- .NOTES
- Command Arguments:
- 1. Chkdsk
- /Scan - Runs an online scan on the system drive, attempts to fix any corruption, and queues other corruption for fixing on reboot
- 2. SFC
- /ScanNow - Performs a scan of the system files and fixes any corruption
- 3. DISM - Fixes system image corruption, and fixes SFC's system image if it was corrupted
- /Online - Fixes the currently running system image
- /Cleanup-Image - Performs cleanup operations on the image, could remove some unneeded temporary files
- /Restorehealth - Performs a scan of the image and fixes any corruption
-
- #>
- Start-Process PowerShell -ArgumentList "Write-Host '(1/4) Chkdsk' -ForegroundColor Green; Chkdsk /scan;
- Write-Host '`n(2/4) SFC - 1st scan' -ForegroundColor Green; sfc /scannow;
- Write-Host '`n(3/4) DISM' -ForegroundColor Green; DISM /Online /Cleanup-Image /Restorehealth;
- Write-Host '`n(4/4) SFC - 2nd scan' -ForegroundColor Green; sfc /scannow;
- Read-Host '`nPress Enter to Continue'" -verb runas
-}
diff --git a/functions/public/Invoke-WPFPresets.ps1 b/functions/public/Invoke-WPFPresets.ps1
index 1535ffe5f6..2139c47ee9 100644
--- a/functions/public/Invoke-WPFPresets.ps1
+++ b/functions/public/Invoke-WPFPresets.ps1
@@ -2,10 +2,10 @@ function Invoke-WPFPresets {
<#
.SYNOPSIS
- Sets the options in the tweaks panel to the given preset
+ Sets the checkboxes in winutil to the given preset
.PARAMETER preset
- The preset to set the options to
+ The preset to set the checkboxes to
.PARAMETER imported
If the preset is imported from a file, defaults to false
@@ -17,7 +17,7 @@ function Invoke-WPFPresets {
param (
[Parameter(position=0)]
- [Array]$preset = "",
+ [Array]$preset = $null,
[Parameter(position=1)]
[bool]$imported = $false,
@@ -32,33 +32,19 @@ function Invoke-WPFPresets {
$CheckBoxesToCheck = $sync.configs.preset.$preset
}
- $CheckBoxes = ($sync.GetEnumerator()).where{ $_.Value -is [System.Windows.Controls.CheckBox] -and $_.Name -notlike "WPFToggle*" -and $_.Name -like "$checkboxfilterpattern"}
- Write-Debug "Getting checkboxes to set, number of checkboxes: $($CheckBoxes.Count)"
-
- if ($CheckBoxesToCheck -ne "") {
- $debugMsg = "CheckBoxes to Check are: "
- $CheckBoxesToCheck | ForEach-Object { $debugMsg += "$_, " }
- $debugMsg = $debugMsg -replace (',\s*$', '')
- Write-Debug "$debugMsg"
+ # clear out the filtered pattern so applying a preset replaces the current
+ # state rather than merging with it
+ switch ($checkboxfilterpattern) {
+ "WPFTweak*" { $sync.selectedTweaks = [System.Collections.Generic.List[string]]::new() }
+ "WPFInstall*" { $sync.selectedApps = [System.Collections.Generic.List[string]]::new() }
+ "WPFeatures" { $sync.selectedFeatures = [System.Collections.Generic.List[string]]::new() }
+ "WPFToggle" { $sync.selectedToggles = [System.Collections.Generic.List[string]]::new() }
+ default {}
}
- foreach ($CheckBox in $CheckBoxes) {
- $checkboxName = $CheckBox.Key
-
- if (-not $CheckBoxesToCheck) {
- $sync.$checkboxName.IsChecked = $false
- continue
- }
-
- # Check if the checkbox name exists in the flattened JSON hashtable
- if ($CheckBoxesToCheck -contains $checkboxName) {
- # If it exists, set IsChecked to true
- $sync.$checkboxName.IsChecked = $true
- Write-Debug "$checkboxName is checked"
- } else {
- # If it doesn't exist, set IsChecked to false
- $sync.$checkboxName.IsChecked = $false
- Write-Debug "$checkboxName is not checked"
- }
+ if ($preset) {
+ Update-WinUtilSelections -flatJson $CheckBoxesToCheck
}
+
+ Reset-WPFCheckBoxes -doToggles $false -checkboxfilterpattern $checkboxfilterpattern
}
diff --git a/functions/public/Invoke-WPFRunAdobeCCCleanerTool.ps1 b/functions/public/Invoke-WPFRunAdobeCCCleanerTool.ps1
deleted file mode 100644
index 84768974c6..0000000000
--- a/functions/public/Invoke-WPFRunAdobeCCCleanerTool.ps1
+++ /dev/null
@@ -1,32 +0,0 @@
-function Invoke-WPFRunAdobeCCCleanerTool {
- <#
- .SYNOPSIS
- It removes or fixes problem files and resolves permission issues in registry keys.
- .DESCRIPTION
- The Creative Cloud Cleaner tool is a utility for experienced users to clean up corrupted installations.
- #>
-
- [string]$url="https://swupmf.adobe.com/webfeed/CleanerTool/win/AdobeCreativeCloudCleanerTool.exe"
-
- Write-Host "The Adobe Creative Cloud Cleaner tool is hosted at"
- Write-Host "$url"
-
- try {
- # Don't show the progress because it will slow down the download speed
- $ProgressPreference='SilentlyContinue'
-
- Invoke-WebRequest -Uri $url -OutFile "$env:TEMP\AdobeCreativeCloudCleanerTool.exe" -UseBasicParsing -ErrorAction SilentlyContinue -Verbose
-
- # Revert back the ProgressPreference variable to the default value since we got the file desired
- $ProgressPreference='Continue'
-
- Start-Process -FilePath "$env:TEMP\AdobeCreativeCloudCleanerTool.exe" -Wait -ErrorAction SilentlyContinue -Verbose
- } catch {
- Write-Error $_.Exception.Message
- } finally {
- if (Test-Path -Path "$env:TEMP\AdobeCreativeCloudCleanerTool.exe") {
- Write-Host "Cleaning up..."
- Remove-Item -Path "$env:TEMP\AdobeCreativeCloudCleanerTool.exe" -Verbose
- }
- }
-}
diff --git a/functions/public/Invoke-WPFRunspace.ps1 b/functions/public/Invoke-WPFRunspace.ps1
index 67c01b8f3f..bccdd5df2a 100644
--- a/functions/public/Invoke-WPFRunspace.ps1
+++ b/functions/public/Invoke-WPFRunspace.ps1
@@ -27,8 +27,7 @@ function Invoke-WPFRunspace {
Param (
$ScriptBlock,
$ArgumentList,
- $ParameterList,
- $DebugPreference
+ $ParameterList
)
# Create a PowerShell instance
@@ -41,7 +40,7 @@ function Invoke-WPFRunspace {
foreach ($parameter in $ParameterList) {
$script:powershell.AddParameter($parameter[0], $parameter[1])
}
- $script:powershell.AddArgument($DebugPreference) # Pass DebugPreference to the script block
+
$script:powershell.RunspacePool = $sync.runspace
# Execute the RunspacePool
diff --git a/functions/public/Invoke-WPFSSHServer.ps1 b/functions/public/Invoke-WPFSSHServer.ps1
index 503f867736..0ea6de59ad 100644
--- a/functions/public/Invoke-WPFSSHServer.ps1
+++ b/functions/public/Invoke-WPFSSHServer.ps1
@@ -6,7 +6,7 @@ function Invoke-WPFSSHServer {
#>
- Invoke-WPFRunspace -DebugPreference $DebugPreference -ScriptBlock {
+ Invoke-WPFRunspace -ScriptBlock {
Invoke-WinUtilSSHServer
diff --git a/functions/public/Invoke-WPFSelectedCheckboxesUpdate.ps1 b/functions/public/Invoke-WPFSelectedCheckboxesUpdate.ps1
new file mode 100644
index 0000000000..6bdee481f4
--- /dev/null
+++ b/functions/public/Invoke-WPFSelectedCheckboxesUpdate.ps1
@@ -0,0 +1,95 @@
+function Invoke-WPFSelectedCheckboxesUpdate{
+ <#
+ .SYNOPSIS
+ This is a helper function that is called by the Checked and Unchecked events of the Checkboxes.
+ It also Updates the "Selected Apps" selectedAppLabel on the Install Tab to represent the current collection
+ .PARAMETER type
+ Either: Add | Remove
+ .PARAMETER checkboxName
+ should contain the name of the current instance of the checkbox that triggered the Event.
+ Most of the time will be the automatic variable $this.Parent.Tag
+ .EXAMPLE
+ $checkbox.Add_Unchecked({Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $this.Parent.Tag})
+ OR
+ Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $specificCheckbox.Parent.Tag
+ #>
+ param (
+ $type,
+ $checkboxName
+ )
+
+ if (($type -ne "Add") -and ($type -ne "Remove"))
+ {
+ Write-Error "Type: $type not implemented"
+ return
+ }
+
+ # Get the actual Name from the selectedAppLabel inside the Checkbox
+ $appKey = $checkboxName
+ $group = if ($appKey.StartsWith("WPFInstall")) { "Install" }
+ elseif ($appKey.StartsWith("WPFTweaks")) { "Tweaks" }
+ elseif ($appKey.StartsWith("WPFToggle")) { "Toggle" }
+ elseif ($appKey.StartsWith("WPFFeature")) { "Feature" }
+ else { "na" }
+
+ switch ($group) {
+ "Install" {
+ if ($type -eq "Add") {
+ if (!$sync.selectedApps.Contains($appKey)) {
+ $sync.selectedApps.Add($appKey)
+ # The List type needs to be specified again, because otherwise Sort-Object will convert the list to a string if there is only a single entry
+ [System.Collections.Generic.List[string]]$sync.selectedApps = $sync.SelectedApps | Sort-Object
+ }
+ }
+ else{
+ $sync.selectedApps.Remove($appKey)
+ }
+
+ $count = $sync.SelectedApps.Count
+ $sync.WPFselectedAppsButton.Content = "Selected Apps: $count"
+ # On every change, remove all entries inside the Popup Menu. This is done, so we can keep the alphabetical order even if elements are selected in a random way
+ $sync.selectedAppsstackPanel.Children.Clear()
+ $sync.selectedApps | Foreach-Object { Add-SelectedAppsMenuItem -name $($sync.configs.applicationsHashtable.$_.Content) -key $_ }
+ }
+ "Tweaks" {
+ if ($type -eq "Add") {
+ if (!$sync.selectedTweaks.Contains($appKey)) {
+ $sync.selectedTweaks.Add($appKey)
+ }
+ }
+ else{
+ $sync.selectedTweaks.Remove($appKey)
+ }
+ }
+ "Toggle" {
+ if ($type -eq "Add") {
+ if (!$sync.selectedToggles.Contains($appKey)) {
+ $sync.selectedToggles.Add($appKey)
+ }
+ }
+ else{
+ $sync.selectedToggles.Remove($appKey)
+ }
+ }
+ "Feature" {
+ if ($type -eq "Add") {
+ if (!$sync.selectedFeatures.Contains($appKey)) {
+ $sync.selectedFeatures.Add($appKey)
+ }
+ }
+ else{
+ $sync.selectedFeatures.Remove($appKey)
+ }
+ }
+ default {
+ Write-Host "Unknown group for checkbox: $($appKey)"
+ }
+ }
+
+ Write-Debug "-------------------------------------"
+ Write-Debug "Selected Apps: $($sync.selectedApps)"
+ Write-Debug "Selected Tweaks: $($sync.selectedTweaks)"
+ Write-Debug "Selected Toggles: $($sync.selectedToggles)"
+ Write-Debug "Selected Features: $($sync.selectedFeatures)"
+ Write-Debug "--------------------------------------"
+}
diff --git a/functions/public/Invoke-WPFSystemRepair.ps1 b/functions/public/Invoke-WPFSystemRepair.ps1
new file mode 100644
index 0000000000..a44a5c0c25
--- /dev/null
+++ b/functions/public/Invoke-WPFSystemRepair.ps1
@@ -0,0 +1,19 @@
+function Invoke-WPFSystemRepair {
+ <#
+ .SYNOPSIS
+ Checks for system corruption using SFC, and DISM
+ Checks for disk failure using Chkdsk
+
+ .DESCRIPTION
+ 1. Chkdsk - Checks for disk errors, which can cause system file corruption and notifies of early disk failure
+ 2. SFC - scans protected system files for corruption and fixes them
+ 3. DISM - Repair a corrupted Windows operating system image
+ #>
+
+ Start-Process cmd.exe -ArgumentList "/c chkdsk /scan /perf" -NoNewWindow -Wait
+ Start-Process cmd.exe -ArgumentList "/c sfc /scannow" -NoNewWindow -Wait
+ Start-Process cmd.exe -ArgumentList "/c dism /online /cleanup-image /restorehealth" -NoNewWindow -Wait
+
+ Write-Host "==> Finished System Repair"
+ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
+}
diff --git a/functions/public/Invoke-WPFTab.ps1 b/functions/public/Invoke-WPFTab.ps1
index 15d64bbdd0..530a7dc2da 100644
--- a/functions/public/Invoke-WPFTab.ps1
+++ b/functions/public/Invoke-WPFTab.ps1
@@ -28,4 +28,31 @@ function Invoke-WPFTab {
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
}
}
+ $sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
+
+ # Always reset the filter for the current tab
+ if ($sync.currentTab -eq "Install") {
+ # Reset Install tab filter
+ Find-AppsByNameOrDescription -SearchString ""
+ } elseif ($sync.currentTab -eq "Tweaks") {
+ # Reset Tweaks tab filter
+ Find-TweaksByNameOrDescription -SearchString ""
+ }
+
+ # Show search bar in Install and Tweaks tabs
+ if ($tabNumber -eq 0 -or $tabNumber -eq 1) {
+ $sync.SearchBar.Visibility = "Visible"
+ $searchIcon = ($sync.Form.FindName("SearchBar").Parent.Children | Where-Object { $_ -is [System.Windows.Controls.TextBlock] -and $_.Text -eq [char]0xE721 })[0]
+ if ($searchIcon) {
+ $searchIcon.Visibility = "Visible"
+ }
+ } else {
+ $sync.SearchBar.Visibility = "Collapsed"
+ $searchIcon = ($sync.Form.FindName("SearchBar").Parent.Children | Where-Object { $_ -is [System.Windows.Controls.TextBlock] -and $_.Text -eq [char]0xE721 })[0]
+ if ($searchIcon) {
+ $searchIcon.Visibility = "Collapsed"
+ }
+ # Hide the clear button if it's visible
+ $sync.SearchBarClearButton.Visibility = "Collapsed"
+ }
}
diff --git a/functions/public/Invoke-WPFToggleAllCategories.ps1 b/functions/public/Invoke-WPFToggleAllCategories.ps1
new file mode 100644
index 0000000000..6b0745d175
--- /dev/null
+++ b/functions/public/Invoke-WPFToggleAllCategories.ps1
@@ -0,0 +1,52 @@
+function Invoke-WPFToggleAllCategories {
+ <#
+ .SYNOPSIS
+ Expands or collapses all categories in the Install tab
+
+ .PARAMETER Action
+ The action to perform: "Expand" or "Collapse"
+
+ .DESCRIPTION
+ This function iterates through all category containers in the Install tab
+ and expands or collapses their WrapPanels while updating the toggle button labels
+ #>
+
+ param(
+ [Parameter(Mandatory=$true)]
+ [ValidateSet("Expand", "Collapse")]
+ [string]$Action
+ )
+
+ try {
+ if ($null -eq $sync.ItemsControl) {
+ Write-Warning "ItemsControl not initialized"
+ return
+ }
+
+ $targetVisibility = if ($Action -eq "Expand") { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed }
+ $targetPrefix = if ($Action -eq "Expand") { "-" } else { "+" }
+ $sourcePrefix = if ($Action -eq "Expand") { "+" } else { "-" }
+
+ # Iterate through all items in the ItemsControl
+ $sync.ItemsControl.Items | ForEach-Object {
+ $categoryContainer = $_
+
+ # Check if this is a category container (StackPanel with children)
+ if ($categoryContainer -is [System.Windows.Controls.StackPanel] -and $categoryContainer.Children.Count -ge 2) {
+ # Get the WrapPanel (second child)
+ $wrapPanel = $categoryContainer.Children[1]
+ $wrapPanel.Visibility = $targetVisibility
+
+ # Update the label to show the correct state
+ $categoryLabel = $categoryContainer.Children[0]
+ if ($categoryLabel.Content -like "$sourcePrefix*") {
+ $escapedSourcePrefix = [regex]::Escape($sourcePrefix)
+ $categoryLabel.Content = $categoryLabel.Content -replace "^$escapedSourcePrefix ", "$targetPrefix "
+ }
+ }
+ }
+ }
+ catch {
+ Write-Error "Error toggling categories: $_"
+ }
+}
diff --git a/functions/public/Invoke-WPFTweakPS7.ps1 b/functions/public/Invoke-WPFTweakPS7.ps1
deleted file mode 100644
index 6b1959c276..0000000000
--- a/functions/public/Invoke-WPFTweakPS7.ps1
+++ /dev/null
@@ -1,53 +0,0 @@
-function Invoke-WPFTweakPS7{
- <#
- .SYNOPSIS
- This will edit the config file of the Windows Terminal Replacing the Powershell 5 to Powershell 7 and install Powershell 7 if necessary
- .PARAMETER action
- PS7: Configures Powershell 7 to be the default Terminal
- PS5: Configures Powershell 5 to be the default Terminal
- #>
- param (
- [ValidateSet("PS7", "PS5")]
- [string]$action
- )
-
- switch ($action) {
- "PS7"{
- if (Test-Path -Path "$env:ProgramFiles\PowerShell\7") {
- Write-Host "Powershell 7 is already installed."
- } else {
- Write-Host "Installing Powershell 7..."
- Install-WinUtilProgramWinget -Action Install -Programs @("Microsoft.PowerShell")
- }
- $targetTerminalName = "PowerShell"
- }
- "PS5"{
- $targetTerminalName = "Windows PowerShell"
- }
- }
- # Check if the Windows Terminal is installed and return if not (Prerequisite for the following code)
- if (-not (Get-Command "wt" -ErrorAction SilentlyContinue)) {
- Write-Host "Windows Terminal not installed. Skipping Terminal preference"
- return
- }
- # Check if the Windows Terminal settings.json file exists and return if not (Prereqisite for the following code)
- $settingsPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
- if (-not (Test-Path -Path $settingsPath)) {
- Write-Host "Windows Terminal Settings file not found at $settingsPath"
- return
- }
-
- Write-Host "Settings file found."
- $settingsContent = Get-Content -Path $settingsPath | ConvertFrom-Json
- $ps7Profile = $settingsContent.profiles.list | Where-Object { $_.name -eq $targetTerminalName }
- if ($ps7Profile) {
- $settingsContent.defaultProfile = $ps7Profile.guid
- $updatedSettings = $settingsContent | ConvertTo-Json -Depth 100
- Set-Content -Path $settingsPath -Value $updatedSettings
- Write-Host "Default profile updated to " -NoNewline
- Write-Host "$targetTerminalName " -ForegroundColor White -NoNewline
- Write-Host "using the name attribute."
- } else {
- Write-Host "No PowerShell 7 profile found in Windows Terminal settings using the name attribute."
- }
-}
diff --git a/functions/public/Invoke-WPFUIElements.ps1 b/functions/public/Invoke-WPFUIElements.ps1
index 7b55b84367..df85084bf9 100644
--- a/functions/public/Invoke-WPFUIElements.ps1
+++ b/functions/public/Invoke-WPFUIElements.ps1
@@ -11,26 +11,26 @@ function Invoke-WPFUIElements {
.EXAMPLE
Invoke-WPFUIElements -configVariable $sync.configs.applications -targetGridName "install" -columncount 5
.NOTES
- Future me/contributer: If possible please wrap this into a runspace to make it load all panels at the same time.
+ Future me/contributor: If possible, please wrap this into a runspace to make it load all panels at the same time.
#>
param(
- [Parameter(Mandatory, position=0)]
+ [Parameter(Mandatory, Position = 0)]
[PSCustomObject]$configVariable,
- [Parameter(Mandatory, position=1)]
+ [Parameter(Mandatory, Position = 1)]
[string]$targetGridName,
- [Parameter(Mandatory, position=2)]
+ [Parameter(Mandatory, Position = 2)]
[int]$columncount
)
- $window = $sync["Form"]
+ $window = $sync.form
- $theme = $sync.Form.Resources
$borderstyle = $window.FindResource("BorderStyle")
$HoverTextBlockStyle = $window.FindResource("HoverTextBlockStyle")
$ColorfulToggleSwitchStyle = $window.FindResource("ColorfulToggleSwitchStyle")
+ $ToggleButtonStyle = $window.FindResource("ToggleButtonStyle")
if (!$borderstyle -or !$HoverTextBlockStyle -or !$ColorfulToggleSwitchStyle) {
throw "Failed to retrieve Styles using 'FindResource' from main window element."
@@ -59,6 +59,8 @@ function Invoke-WPFUIElements {
$configHashtable[$_] = $configVariable.$_
}
+ $radioButtonGroups = @{}
+
$organizedData = @{}
# Iterate through JSON data and organize by panel and category
foreach ($entry in $configHashtable.Keys) {
@@ -66,19 +68,17 @@ function Invoke-WPFUIElements {
# Create an object for the application
$entryObject = [PSCustomObject]@{
- Name = $entry
- Order = $entryInfo.order
- Category = $entryInfo.Category
- Content = $entryInfo.Content
- Choco = $entryInfo.choco
- Winget = $entryInfo.winget
- Panel = if ($entryInfo.Panel) { $entryInfo.Panel } else { "0" }
- Link = $entryInfo.link
+ Name = $entry
+ Category = $entryInfo.Category
+ Content = $entryInfo.Content
+ Panel = if ($entryInfo.Panel) { $entryInfo.Panel } else { "0" }
+ Link = $entryInfo.link
Description = $entryInfo.description
- Type = $entryInfo.type
- ComboItems = $entryInfo.ComboItems
- Checked = $entryInfo.Checked
+ Type = $entryInfo.type
+ ComboItems = $entryInfo.ComboItems
+ Checked = $entryInfo.Checked
ButtonWidth = $entryInfo.ButtonWidth
+ GroupName = $entryInfo.GroupName # Added for RadioButton groupings
}
if (-not $organizedData.ContainsKey($entryObject.Panel)) {
@@ -92,15 +92,11 @@ function Invoke-WPFUIElements {
# Store application data in an array under the category
$organizedData[$entryObject.Panel][$entryObject.Category] += $entryObject
- # Only apply the logic for distributing entries across columns if the targetGridName is "appspanel"
- if ($targetGridName -eq "appspanel") {
- $panelcount = 0
- $entrycount = $configHashtable.Keys.Count + $organizedData["0"].Keys.Count
- $maxcount = [Math]::Round($entrycount / $columncount + 0.5)
- }
-
}
+ # Initialize panel count
+ $panelcount = 0
+
# Iterate through 'organizedData' by panel, category, and application
$count = 0
foreach ($panelKey in ($organizedData.Keys | Sort-Object)) {
@@ -111,84 +107,62 @@ function Invoke-WPFUIElements {
$border.style = $borderstyle
$targetGrid.Children.Add($border) | Out-Null
- # Create a StackPanel inside the Border
- $stackPanel = New-Object Windows.Controls.StackPanel
- $stackPanel.Background = [Windows.Media.Brushes]::Transparent
- $stackPanel.SnapsToDevicePixels = $true
- $stackPanel.VerticalAlignment = "Stretch"
- $border.Child = $stackPanel
- $panelcount++
+ # Use a DockPanel to contain the content
+ $dockPanelContainer = New-Object Windows.Controls.DockPanel
+ $border.Child = $dockPanelContainer
- # Add Windows Version label if this is the updates panel
- if ($targetGridName -eq "updatespanel") {
- $windowsVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").ProductName
- $versionLabel = New-Object Windows.Controls.Label
- $versionLabel.Content = "Windows Version: $windowsVersion"
- $versionLabel.FontSize = $theme.FontSize
- $versionLabel.HorizontalAlignment = "Left"
- $stackPanel.Children.Add($versionLabel) | Out-Null
- }
+ # Create an ItemsControl for application content
+ $itemsControl = New-Object Windows.Controls.ItemsControl
+ $itemsControl.HorizontalAlignment = 'Stretch'
+ $itemsControl.VerticalAlignment = 'Stretch'
+
+ # Set the ItemsPanel to a VirtualizingStackPanel
+ $itemsPanelTemplate = New-Object Windows.Controls.ItemsPanelTemplate
+ $factory = New-Object Windows.FrameworkElementFactory ([Windows.Controls.VirtualizingStackPanel])
+ $itemsPanelTemplate.VisualTree = $factory
+ $itemsControl.ItemsPanel = $itemsPanelTemplate
+
+ # Set virtualization properties
+ $itemsControl.SetValue([Windows.Controls.VirtualizingStackPanel]::IsVirtualizingProperty, $true)
+ $itemsControl.SetValue([Windows.Controls.VirtualizingStackPanel]::VirtualizationModeProperty, [Windows.Controls.VirtualizationMode]::Recycling)
+ # Add the ItemsControl directly to the DockPanel
+ [Windows.Controls.DockPanel]::SetDock($itemsControl, [Windows.Controls.Dock]::Bottom)
+ $dockPanelContainer.Children.Add($itemsControl) | Out-Null
+ $panelcount++
+
+ # Now proceed with adding category labels and entries to $itemsControl
foreach ($category in ($organizedData[$panelKey].Keys | Sort-Object)) {
$count++
- if ($targetGridName -eq "appspanel" -and $columncount -gt 0) {
- $panelcount2 = [Int](($count) / $maxcount - 0.5)
- if ($panelcount -eq $panelcount2) {
- # Create a new Border for the new column
- $border = New-Object Windows.Controls.Border
- $border.VerticalAlignment = "Stretch"
- [System.Windows.Controls.Grid]::SetColumn($border, $panelcount)
- $border.style = $borderstyle
- $targetGrid.Children.Add($border) | Out-Null
-
- # Create a new StackPanel inside the Border
- $stackPanel = New-Object Windows.Controls.StackPanel
- $stackPanel.Background = [Windows.Media.Brushes]::Transparent
- $stackPanel.SnapsToDevicePixels = $true
- $stackPanel.VerticalAlignment = "Stretch"
- $border.Child = $stackPanel
- $panelcount++
- }
- }
$label = New-Object Windows.Controls.Label
$label.Content = $category -replace ".*__", ""
- $label.FontSize = $theme.HeadingFontSize
- $label.FontFamily = $theme.HeaderFontFamily
- $stackPanel.Children.Add($label) | Out-Null
-
+ $label.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "HeaderFontSize")
+ $label.SetResourceReference([Windows.Controls.Control]::FontFamilyProperty, "HeaderFontFamily")
+ $label.UseLayoutRounding = $true
+ $itemsControl.Items.Add($label) | Out-Null
$sync[$category] = $label
- # Sort entries by Order and then by Name, but only display Name
- $entries = $organizedData[$panelKey][$category] | Sort-Object Order, Name
+ # Sort entries by type (checkboxes first, then buttons, then comboboxes) and then alphabetically by Content
+ $entries = $organizedData[$panelKey][$category] | Sort-Object @{Expression = {
+ switch ($_.Type) {
+ 'Button' { 1 }
+ 'Combobox' { 2 }
+ default { 0 }
+ }
+ }}, Content
foreach ($entryInfo in $entries) {
$count++
- if ($targetGridName -eq "appspanel" -and $columncount -gt 0) {
- $panelcount2 = [Int](($count) / $maxcount - 0.5)
- if ($panelcount -eq $panelcount2) {
- # Create a new Border for the new column
- $border = New-Object Windows.Controls.Border
- $border.VerticalAlignment = "Stretch"
- [System.Windows.Controls.Grid]::SetColumn($border, $panelcount)
- $border.style = $borderstyle
- $targetGrid.Children.Add($border) | Out-Null
-
- # Create a new StackPanel inside the Border
- $stackPanel = New-Object Windows.Controls.StackPanel
- $stackPanel.Background = [Windows.Media.Brushes]::Transparent
- $stackPanel.SnapsToDevicePixels = $true
- $stackPanel.VerticalAlignment = "Stretch"
- $border.Child = $stackPanel
- $panelcount++
- }
- }
-
+ # Create the UI elements based on the entry type
switch ($entryInfo.Type) {
"Toggle" {
$dockPanel = New-Object Windows.Controls.DockPanel
+ [System.Windows.Automation.AutomationProperties]::SetName($dockPanel, $entryInfo.Content)
$checkBox = New-Object Windows.Controls.CheckBox
$checkBox.Name = $entryInfo.Name
$checkBox.HorizontalAlignment = "Right"
+ $checkBox.UseLayoutRounding = $true
+ [System.Windows.Automation.AutomationProperties]::SetName($checkBox, $entryInfo.Content)
$dockPanel.Children.Add($checkBox) | Out-Null
$checkBox.Style = $ColorfulToggleSwitchStyle
@@ -196,90 +170,125 @@ function Invoke-WPFUIElements {
$label.Content = $entryInfo.Content
$label.ToolTip = $entryInfo.Description
$label.HorizontalAlignment = "Left"
- $label.FontSize = $theme.FontSize
+ $label.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "FontSize")
$label.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "MainForegroundColor")
+ $label.UseLayoutRounding = $true
$dockPanel.Children.Add($label) | Out-Null
- $stackPanel.Children.Add($dockPanel) | Out-Null
+ $itemsControl.Items.Add($dockPanel) | Out-Null
$sync[$entryInfo.Name] = $checkBox
-
- $sync[$entryInfo.Name].IsChecked = (Get-WinUtilToggleStatus $entryInfo.Name)
-
- $sync[$entryInfo.Name].Add_Checked({
- [System.Object]$Sender = $args[0]
- Invoke-WinUtilTweaks $sender.name
- })
-
- $sync[$entryInfo.Name].Add_Unchecked({
- [System.Object]$Sender = $args[0]
- Invoke-WinUtiltweaks $sender.name -undo $true
- })
+ if ($entryInfo.Name -eq "WPFToggleFOSSHighlight") {
+ if ($entryInfo.Checked -eq $true) {
+ $sync[$entryInfo.Name].IsChecked = $true
+ }
+
+ $sync[$entryInfo.Name].Add_Checked({
+ Invoke-WPFButton -Button "WPFToggleFOSSHighlight"
+ })
+ $sync[$entryInfo.Name].Add_Unchecked({
+ Invoke-WPFButton -Button "WPFToggleFOSSHighlight"
+ })
+ } else {
+ $sync[$entryInfo.Name].IsChecked = (Get-WinUtilToggleStatus $entryInfo.Name)
+
+ $sync[$entryInfo.Name].Add_Checked({
+ [System.Object]$Sender = $args[0]
+ Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $Sender.name
+ # Skip applying tweaks while an import is restoring toggle states
+ if (-not $sync.ImportInProgress) {
+ Invoke-WinUtilTweaks $Sender.name
+ }
+ })
+
+ $sync[$entryInfo.Name].Add_Unchecked({
+ [System.Object]$Sender = $args[0]
+ Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $Sender.name
+ # Skip undoing tweaks while an import is restoring toggle states
+ if (-not $sync.ImportInProgress) {
+ Invoke-WinUtiltweaks $Sender.name -undo $true
+ }
+ })
+ }
}
"ToggleButton" {
- $toggleButton = New-Object Windows.Controls.ToggleButton
+ $toggleButton = New-Object Windows.Controls.Primitives.ToggleButton
$toggleButton.Name = $entryInfo.Name
- $toggleButton.Name = "WPFTab" + ($stackPanel.Children.Count + 1) + "BT"
+ $toggleButton.Content = $entryInfo.Content[1]
+ $toggleButton.ToolTip = $entryInfo.Description
$toggleButton.HorizontalAlignment = "Left"
- $toggleButton.Height = $theme.TabButtonHeight
- $toggleButton.Width = $theme.TabButtonWidth
- $toggleButton.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "ButtonInstallBackgroundColor")
- $toggleButton.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "MainForegroundColor")
- $toggleButton.FontWeight = [Windows.FontWeights]::Bold
-
- $textBlock = New-Object Windows.Controls.TextBlock
- $textBlock.FontSize = $theme.TabButtonFontSize
- $textBlock.Background = [Windows.Media.Brushes]::Transparent
- $textBlock.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "ButtonInstallForegroundColor")
-
- $underline = New-Object Windows.Documents.Underline
- $underline.Inlines.Add($entryInfo.name -replace "(.).*", "`$1")
+ $toggleButton.Style = $ToggleButtonStyle
+ [System.Windows.Automation.AutomationProperties]::SetName($toggleButton, $entryInfo.Content[0])
- $run = New-Object Windows.Documents.Run
- $run.Text = $entryInfo.name -replace "^.", ""
+ $toggleButton.Tag = @{
+ contentOn = if ($entryInfo.Content.Count -ge 1) { $entryInfo.Content[0] } else { "" }
+ contentOff = if ($entryInfo.Content.Count -ge 2) { $entryInfo.Content[1] } else { $contentOn }
+ }
- $textBlock.Inlines.Add($underline)
- $textBlock.Inlines.Add($run)
+ $itemsControl.Items.Add($toggleButton) | Out-Null
- $toggleButton.Content = $textBlock
+ $sync[$entryInfo.Name] = $toggleButton
- $stackPanel.Children.Add($toggleButton) | Out-Null
+ $sync[$entryInfo.Name].Add_Checked({
+ $this.Content = $this.Tag.contentOn
+ })
- $sync[$entryInfo.Name] = $toggleButton
+ $sync[$entryInfo.Name].Add_Unchecked({
+ $this.Content = $this.Tag.contentOff
+ })
}
"Combobox" {
$horizontalStackPanel = New-Object Windows.Controls.StackPanel
$horizontalStackPanel.Orientation = "Horizontal"
$horizontalStackPanel.Margin = "0,5,0,0"
+ [System.Windows.Automation.AutomationProperties]::SetName($horizontalStackPanel, $entryInfo.Content)
$label = New-Object Windows.Controls.Label
$label.Content = $entryInfo.Content
$label.HorizontalAlignment = "Left"
$label.VerticalAlignment = "Center"
- $label.FontSize = $theme.ButtonFontSize
+ $label.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "ButtonFontSize")
+ $label.UseLayoutRounding = $true
$horizontalStackPanel.Children.Add($label) | Out-Null
$comboBox = New-Object Windows.Controls.ComboBox
$comboBox.Name = $entryInfo.Name
- $comboBox.Height = $theme.ButtonHeight
- $comboBox.Width = $theme.ButtonWidth
+ $comboBox.SetResourceReference([Windows.Controls.Control]::HeightProperty, "ButtonHeight")
+ $comboBox.SetResourceReference([Windows.Controls.Control]::WidthProperty, "ButtonWidth")
$comboBox.HorizontalAlignment = "Left"
$comboBox.VerticalAlignment = "Center"
- $comboBox.Margin = $theme.ButtonMargin
+ $comboBox.SetResourceReference([Windows.Controls.Control]::MarginProperty, "ButtonMargin")
+ $comboBox.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "ButtonFontSize")
+ $comboBox.UseLayoutRounding = $true
+ [System.Windows.Automation.AutomationProperties]::SetName($comboBox, $entryInfo.Content)
foreach ($comboitem in ($entryInfo.ComboItems -split " ")) {
$comboBoxItem = New-Object Windows.Controls.ComboBoxItem
$comboBoxItem.Content = $comboitem
- $comboBoxItem.FontSize = $theme.ButtonFontSize
+ $comboBoxItem.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "ButtonFontSize")
+ $comboBoxItem.UseLayoutRounding = $true
$comboBox.Items.Add($comboBoxItem) | Out-Null
}
$horizontalStackPanel.Children.Add($comboBox) | Out-Null
- $stackPanel.Children.Add($horizontalStackPanel) | Out-Null
+ $itemsControl.Items.Add($horizontalStackPanel) | Out-Null
$comboBox.SelectedIndex = 0
+ # Set initial text
+ if ($comboBox.Items.Count -gt 0) {
+ $comboBox.Text = $comboBox.Items[0].Content
+ }
+
+ # Add SelectionChanged event handler to update the text property
+ $comboBox.Add_SelectionChanged({
+ $selectedItem = $this.SelectedItem
+ if ($selectedItem) {
+ $this.Text = $selectedItem.Content
+ }
+ })
+
$sync[$entryInfo.Name] = $comboBox
}
@@ -288,26 +297,68 @@ function Invoke-WPFUIElements {
$button.Name = $entryInfo.Name
$button.Content = $entryInfo.Content
$button.HorizontalAlignment = "Left"
- $button.Margin = $theme.ButtonMargin
- $button.FontSize = $theme.ButtonFontSize
+ $button.SetResourceReference([Windows.Controls.Control]::MarginProperty, "ButtonMargin")
+ $button.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "ButtonFontSize")
if ($entryInfo.ButtonWidth) {
- $button.Width = $entryInfo.ButtonWidth
+ $baseWidth = [int]$entryInfo.ButtonWidth
+ $button.Width = [math]::Max($baseWidth, 350)
}
- $stackPanel.Children.Add($button) | Out-Null
+ [System.Windows.Automation.AutomationProperties]::SetName($button, $entryInfo.Content)
+ $itemsControl.Items.Add($button) | Out-Null
$sync[$entryInfo.Name] = $button
}
+ "RadioButton" {
+ # Check if a container for this GroupName already exists
+ if (-not $radioButtonGroups.ContainsKey($entryInfo.GroupName)) {
+ # Create a StackPanel for this group
+ $groupStackPanel = New-Object Windows.Controls.StackPanel
+ $groupStackPanel.Orientation = "Vertical"
+ [System.Windows.Automation.AutomationProperties]::SetName($groupStackPanel, $entryInfo.GroupName)
+
+ # Add the group container to the ItemsControl
+ $itemsControl.Items.Add($groupStackPanel) | Out-Null
+ }
+ else {
+ # Retrieve the existing group container
+ $groupStackPanel = $radioButtonGroups[$entryInfo.GroupName]
+ }
+
+ # Create the RadioButton
+ $radioButton = New-Object Windows.Controls.RadioButton
+ $radioButton.Name = $entryInfo.Name
+ $radioButton.GroupName = $entryInfo.GroupName
+ $radioButton.Content = $entryInfo.Content
+ $radioButton.HorizontalAlignment = "Left"
+ $radioButton.SetResourceReference([Windows.Controls.Control]::MarginProperty, "CheckBoxMargin")
+ $radioButton.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "ButtonFontSize")
+ $radioButton.ToolTip = $entryInfo.Description
+ $radioButton.UseLayoutRounding = $true
+ [System.Windows.Automation.AutomationProperties]::SetName($radioButton, $entryInfo.Content)
+
+ if ($entryInfo.Checked -eq $true) {
+ $radioButton.IsChecked = $true
+ }
+
+ # Add the RadioButton to the group container
+ $groupStackPanel.Children.Add($radioButton) | Out-Null
+ $sync[$entryInfo.Name] = $radioButton
+ }
+
default {
$horizontalStackPanel = New-Object Windows.Controls.StackPanel
$horizontalStackPanel.Orientation = "Horizontal"
+ [System.Windows.Automation.AutomationProperties]::SetName($horizontalStackPanel, $entryInfo.Content)
$checkBox = New-Object Windows.Controls.CheckBox
$checkBox.Name = $entryInfo.Name
$checkBox.Content = $entryInfo.Content
- $checkBox.FontSize = $theme.FontSize
+ $checkBox.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "FontSize")
$checkBox.ToolTip = $entryInfo.Description
- $checkBox.Margin = $theme.CheckBoxMargin
+ $checkBox.SetResourceReference([Windows.Controls.Control]::MarginProperty, "CheckBoxMargin")
+ $checkBox.UseLayoutRounding = $true
+ [System.Windows.Automation.AutomationProperties]::SetName($checkBox, $entryInfo.Content)
if ($entryInfo.Checked -eq $true) {
$checkBox.IsChecked = $entryInfo.Checked
}
@@ -319,14 +370,25 @@ function Invoke-WPFUIElements {
$textBlock.Text = "(?)"
$textBlock.ToolTip = $entryInfo.Link
$textBlock.Style = $HoverTextBlockStyle
+ $textBlock.UseLayoutRounding = $true
$horizontalStackPanel.Children.Add($textBlock) | Out-Null
$sync[$textBlock.Name] = $textBlock
}
- $stackPanel.Children.Add($horizontalStackPanel) | Out-Null
+ $itemsControl.Items.Add($horizontalStackPanel) | Out-Null
$sync[$entryInfo.Name] = $checkBox
+
+ $sync[$entryInfo.Name].Add_Checked({
+ [System.Object]$Sender = $args[0]
+ Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $Sender.name
+ })
+
+ $sync[$entryInfo.Name].Add_Unchecked({
+ [System.Object]$Sender = $args[0]
+ Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkbox $Sender.name
+ })
}
}
}
diff --git a/functions/public/Invoke-WPFUIThread.ps1 b/functions/public/Invoke-WPFUIThread.ps1
new file mode 100644
index 0000000000..d5c1a46586
--- /dev/null
+++ b/functions/public/Invoke-WPFUIThread.ps1
@@ -0,0 +1,21 @@
+function Invoke-WPFUIThread {
+ <#
+
+ .SYNOPSIS
+ Creates and runs a task on Winutil's WPF Forms thread.
+
+ .PARAMETER ScriptBlock
+ The scriptblock to invoke in the thread
+ #>
+
+ [CmdletBinding()]
+ Param (
+ $ScriptBlock
+ )
+
+ if ($PARAM_NOUI) {
+ return;
+ }
+
+ $sync.form.Dispatcher.Invoke([action]$ScriptBlock)
+}
diff --git a/functions/public/Invoke-WPFUltimatePerformance.ps1 b/functions/public/Invoke-WPFUltimatePerformance.ps1
index 8fe24c9522..2d7cc5a438 100644
--- a/functions/public/Invoke-WPFUltimatePerformance.ps1
+++ b/functions/public/Invoke-WPFUltimatePerformance.ps1
@@ -1,75 +1,35 @@
-Function Invoke-WPFUltimatePerformance {
- <#
-
- .SYNOPSIS
- Enables or disables the Ultimate Performance power scheme based on its GUID.
-
- .PARAMETER State
- Specifies whether to "Enable" or "Disable" the Ultimate Performance power scheme.
-
- #>
- param($State)
-
- try {
- # GUID of the Ultimate Performance power plan
- $ultimateGUID = "e9a42b02-d5df-448d-aa00-03f14749eb61"
-
- if ($State -eq "Enable") {
- # Duplicate the Ultimate Performance power plan using its GUID
- $duplicateOutput = powercfg /duplicatescheme $ultimateGUID
-
- $guid = $null
- $nameFromFile = "ChrisTitus - Ultimate Power Plan"
- $description = "Ultimate Power Plan, added via WinUtils"
-
- # Extract the new GUID from the duplicateOutput
- foreach ($line in $duplicateOutput) {
- if ($line -match "\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b") {
- $guid = $matches[0] # $matches[0] will contain the first match, which is the GUID
- Write-Output "GUID: $guid has been extracted and stored in the variable."
- break
+function Invoke-WPFUltimatePerformance {
+ param(
+ [switch]$Do
+ )
+
+ if ($Do) {
+ if (-not (powercfg /list | Select-String "ChrisTitus - Ultimate Power Plan")) {
+ if (-not (powercfg /list | Select-String "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c")) {
+ powercfg /restoredefaultschemes
+ if (-not (powercfg /list | Select-String "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c")) {
+ Write-Host "Failed to restore High Performance plan. Default plans do not include high performance. If you are on a laptop, do NOT use High Performance or Ultimate Performance plans." -ForegroundColor Red
+ return
}
}
-
- if (-not $guid) {
- Write-Output "No GUID found in the duplicateOutput. Check the output format."
- exit 1
- }
-
- # Change the name of the power plan and set its description
- $changeNameOutput = powercfg /changename $guid "$nameFromFile" "$description"
- Write-Output "The power plan name and description have been changed. Output:"
- Write-Output $changeNameOutput
-
- # Set the duplicated Ultimate Performance plan as active
- $setActiveOutput = powercfg /setactive $guid
- Write-Output "The power plan has been set as active. Output:"
- Write-Output $setActiveOutput
-
- Write-Host "> Ultimate Performance plan installed and set as active."
-
- } elseif ($State -eq "Disable") {
- # Check if the Ultimate Performance plan is installed by GUID
- $installedPlan = (powercfg -list | Select-String -Pattern "ChrisTitus - Ultimate Power Plan").Line.Split()[3]
-
- if ($installedPlan) {
- # Extract the GUID of the installed Ultimate Performance plan
- $ultimatePlanGUID = $installedPlan.Line.Split()[3]
-
- # Set a different power plan as active before deleting the Ultimate Performance plan
- $balancedPlanGUID = 381b4222-f694-41f0-9685-ff5bb260df2e
- powercfg -setactive $balancedPlanGUID
-
- # Delete the Ultimate Performance plan by GUID
- powercfg -delete $ultimatePlanGUID
-
- Write-Host "Ultimate Performance plan has been uninstalled."
- Write-Host "> Balanced plan is now active."
- } else {
- Write-Host "Ultimate Performance plan is not installed."
- }
+ $guid = ((powercfg /duplicatescheme 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c) -split '\s+')[3]
+ powercfg /changename $guid "ChrisTitus - Ultimate Power Plan"
+ powercfg /setacvalueindex $guid SUB_PROCESSOR IDLEDISABLE 1
+ powercfg /setacvalueindex $guid 54533251-82be-4824-96c1-47b60b740d00 4d2b0152-7d5c-498b-88e2-34345392a2c5 1
+ powercfg /setacvalueindex $guid SUB_PROCESSOR PROCTHROTTLEMIN 100
+ powercfg /setactive $guid
+ Write-Host "ChrisTitus - Ultimate Power Plan plan installed and activated." -ForegroundColor Green
+ } else {
+ Write-Host "ChrisTitus - Ultimate Power Plan plan is already installed." -ForegroundColor Red
+ return
+ }
+ } else {
+ if (powercfg /list | Select-String "ChrisTitus - Ultimate Power Plan") {
+ powercfg /setactive SCHEME_BALANCED
+ powercfg /delete ((powercfg /list | Select-String "ChrisTitus - Ultimate Power Plan").ToString().Split()[3])
+ Write-Host "ChrisTitus - Ultimate Power Plan plan was removed." -ForegroundColor Red
+ } else {
+ Write-Host "ChrisTitus - Ultimate Power Plan plan is not installed." -ForegroundColor Yellow
}
- } catch {
- Write-Error "Error occurred: $_"
}
}
diff --git a/functions/public/Invoke-WPFUnInstall.ps1 b/functions/public/Invoke-WPFUnInstall.ps1
index 7180ecf20e..1110faeacb 100644
--- a/functions/public/Invoke-WPFUnInstall.ps1
+++ b/functions/public/Invoke-WPFUnInstall.ps1
@@ -1,9 +1,12 @@
function Invoke-WPFUnInstall {
+ param(
+ [Parameter(Mandatory=$false)]
+ [PSObject[]]$PackagesToUninstall = $($sync.selectedApps | Foreach-Object { $sync.configs.applicationsHashtable.$_ })
+ )
<#
.SYNOPSIS
Uninstalls the selected programs
-
#>
if($sync.ProcessRunning) {
@@ -12,9 +15,7 @@ function Invoke-WPFUnInstall {
return
}
- $PackagesToInstall = (Get-WinUtilCheckBoxes)["Install"]
-
- if ($PackagesToInstall.Count -eq 0) {
+ if ($PackagesToUninstall.Count -eq 0) {
$WarningMsg = "Please select the program(s) to uninstall"
[System.Windows.MessageBox]::Show($WarningMsg, $AppTitle, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
@@ -22,68 +23,43 @@ function Invoke-WPFUnInstall {
$ButtonType = [System.Windows.MessageBoxButton]::YesNo
$MessageboxTitle = "Are you sure?"
- $Messageboxbody = ("This will uninstall the following applications: `n $($PackagesToInstall | Format-Table | Out-String)")
+ $Messageboxbody = ("This will uninstall the following applications: `n $($PackagesToUninstall | Select-Object Name, Description| Out-String)")
$MessageIcon = [System.Windows.MessageBoxImage]::Information
$confirm = [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
if($confirm -eq "No") {return}
- $ChocoPreference = $($sync.WPFpreferChocolatey.IsChecked)
- Invoke-WPFRunspace -ArgumentList @(("PackagesToInstall", $PackagesToInstall),("ChocoPreference", $ChocoPreference)) -DebugPreference $DebugPreference -ScriptBlock {
- param($PackagesToInstall, $ChocoPreference, $DebugPreference)
- if ($PackagesToInstall.count -eq 1) {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" })
- } else {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" })
- }
- $packagesWinget, $packagesChoco = {
- $packagesWinget = [System.Collections.ArrayList]::new()
- $packagesChoco = [System.Collections.ArrayList]::new()
+ $ManagerPreference = $sync.preferences.packagemanager
- foreach ($package in $PackagesToInstall) {
- if ($ChocoPreference) {
- if ($package.choco -eq "na") {
- $packagesWinget.add($package.winget)
- Write-Host "Queueing $($package.winget) for Winget uninstall"
- } else {
- $null = $packagesChoco.add($package.choco)
- Write-Host "Queueing $($package.choco) for Chocolatey uninstall"
- }
- }
- else {
- if ($package.winget -eq "na") {
- $packagesChoco.add($package.choco)
- Write-Host "Queueing $($package.choco) for Chocolatey uninstall"
- } else {
- $null = $packagesWinget.add($($package.winget))
- Write-Host "Queueing $($package.winget) for Winget uninstall"
- }
- }
- }
- return $packagesWinget, $packagesChoco
- }.Invoke($PackagesToInstall)
+ Invoke-WPFRunspace -ParameterList @(("PackagesToUninstall", $PackagesToUninstall),("ManagerPreference", $ManagerPreference)) -ScriptBlock {
+ param($PackagesToUninstall, $ManagerPreference)
+
+ $packagesSorted = Get-WinUtilSelectedPackages -PackageList $PackagesToUninstall -Preference $ManagerPreference
+ $packagesWinget = $packagesSorted[[PackageManagers]::Winget]
+ $packagesChoco = $packagesSorted[[PackageManagers]::Choco]
try {
$sync.ProcessRunning = $true
+ Show-WPFInstallAppBusy -text "Uninstalling apps..."
- # Install all selected programs in new window
+ # Uninstall all selected programs in new window
if($packagesWinget.Count -gt 0) {
Install-WinUtilProgramWinget -Action Uninstall -Programs $packagesWinget
}
if($packagesChoco.Count -gt 0) {
Install-WinUtilProgramChoco -Action Uninstall -Programs $packagesChoco
}
-
+ Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "-- Uninstalls have finished ---"
Write-Host "==========================================="
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
} catch {
Write-Host "==========================================="
Write-Host "Error: $_"
Write-Host "==========================================="
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Error" -overlay "warning" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
$sync.ProcessRunning = $False
diff --git a/functions/public/Invoke-WPFUpdatesdefault.ps1 b/functions/public/Invoke-WPFUpdatesdefault.ps1
index 0b8403c651..afb7609fdc 100644
--- a/functions/public/Invoke-WPFUpdatesdefault.ps1
+++ b/functions/public/Invoke-WPFUpdatesdefault.ps1
@@ -5,61 +5,52 @@ function Invoke-WPFUpdatesdefault {
Resets Windows Update settings to default
#>
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoUpdate" -Type DWord -Value 0
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUOptions" -Type DWord -Value 3
- If (!(Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config")) {
- New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -Type DWord -Value 1
+ $ErrorActionPreference = 'SilentlyContinue'
+
+ Write-Host "Removing Windows Update policy settings..." -ForegroundColor Green
+
+ Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Recurse -Force
+ Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization" -Recurse -Force
+ Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Recurse -Force
+ Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Recurse -Force
+ Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Recurse -Force
+ Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Recurse -Force
+
+ Write-Host "Reenabling Windows Update Services..." -ForegroundColor Green
- $services = @(
- "BITS"
- "wuauserv"
- )
+ Write-Host "Restored BITS to Manual"
+ Set-Service -Name BITS -StartupType Manual
- foreach ($service in $services) {
- # -ErrorAction SilentlyContinue is so it doesn't write an error to stdout if a service doesn't exist
+ Write-Host "Restored wuauserv to Manual"
+ Set-Service -Name wuauserv -StartupType Manual
- Write-Host "Setting $service StartupType to Automatic"
- Get-Service -Name $service -ErrorAction SilentlyContinue | Set-Service -StartupType Automatic
+ Write-Host "Restored UsoSvc to Automatic"
+ Start-Service -Name UsoSvc
+ Set-Service -Name UsoSvc -StartupType Automatic
+
+ Write-Host "Restored WaaSMedicSvc to Manual"
+ Set-Service -Name WaaSMedicSvc -StartupType Manual
+
+ Write-Host "Enabling update related scheduled tasks..." -ForegroundColor Green
+
+ $Tasks =
+ '\Microsoft\Windows\InstallService\*',
+ '\Microsoft\Windows\UpdateOrchestrator\*',
+ '\Microsoft\Windows\UpdateAssistant\*',
+ '\Microsoft\Windows\WaaSMedic\*',
+ '\Microsoft\Windows\WindowsUpdate\*',
+ '\Microsoft\WindowsUpdate\*'
+
+ foreach ($Task in $Tasks) {
+ Get-ScheduledTask -TaskPath $Task | Enable-ScheduledTask -ErrorAction SilentlyContinue
}
- Write-Host "Enabling driver offering through Windows Update..."
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontPromptForWindowsUpdate" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontSearchWindowsUpdate" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DriverUpdateWizardWuSearchEnabled" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -ErrorAction SilentlyContinue
- Write-Host "Enabling Windows Update automatic restart..."
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUPowerManagement" -ErrorAction SilentlyContinue
- Write-Host "Enabled driver offering through Windows Update"
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "BranchReadinessLevel" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferFeatureUpdatesPeriodInDays" -ErrorAction SilentlyContinue
- Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -ErrorAction SilentlyContinue
- Write-Host "==================================================="
- Write-Host "--- Windows Update Settings Reset to Default ---"
- Write-Host "==================================================="
- Start-Process -FilePath "secedit" -ArgumentList "/configure /cfg $env:windir\inf\defltbase.inf /db defltbase.sdb /verbose" -Wait
- Start-Process -FilePath "cmd.exe" -ArgumentList "/c RD /S /Q $env:WinDir\System32\GroupPolicyUsers" -Wait
- Start-Process -FilePath "cmd.exe" -ArgumentList "/c RD /S /Q $env:WinDir\System32\GroupPolicy" -Wait
- Start-Process -FilePath "gpupdate" -ArgumentList "/force" -Wait
- Remove-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKCU:\Software\Microsoft\WindowsSelfHost" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKCU:\Software\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\Microsoft\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\WindowsStore\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\Microsoft\WindowsSelfHost" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\WOW6432Node\Microsoft\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Policies" -Recurse -Force -ErrorAction SilentlyContinue
- Remove-Item -Path "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\WindowsStore\WindowsUpdate" -Recurse -Force -ErrorAction SilentlyContinue
+ Write-Host "Windows Local Policies Reset to Default"
+ secedit /configure /cfg "$Env:SystemRoot\inf\defltbase.inf" /db defltbase.sdb
+
+ Write-Host "===================================================" -ForegroundColor Green
+ Write-Host "--- Windows Update Settings Reset to Default ---" -ForegroundColor Green
+ Write-Host "===================================================" -ForegroundColor Green
- Write-Host "==================================================="
- Write-Host "--- Windows Local Policies Reset to Default ---"
- Write-Host "==================================================="
+ Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
}
diff --git a/functions/public/Invoke-WPFUpdatesdisable.ps1 b/functions/public/Invoke-WPFUpdatesdisable.ps1
index d3f2881563..37c412fd72 100644
--- a/functions/public/Invoke-WPFUpdatesdisable.ps1
+++ b/functions/public/Invoke-WPFUpdatesdisable.ps1
@@ -8,28 +8,47 @@ function Invoke-WPFUpdatesdisable {
Disabling Windows Update is not recommended. This is only for advanced users who know what they are doing.
#>
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force | Out-Null
- }
+ $ErrorActionPreference = 'SilentlyContinue'
+
+ Write-Host "Configuring registry settings..." -ForegroundColor Yellow
+ New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
+
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoUpdate" -Type DWord -Value 1
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUOptions" -Type DWord -Value 1
- If (!(Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config")) {
- New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Force | Out-Null
- }
+
+ New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -Type DWord -Value 0
- $services = @(
- "BITS"
- "wuauserv"
- )
+ Write-Host "Disabled BITS Service"
+ Set-Service -Name BITS -StartupType Disabled
+
+ Write-Host "Disabled wuauserv Service"
+ Set-Service -Name wuauserv -StartupType Disabled
- foreach ($service in $services) {
- # -ErrorAction SilentlyContinue is so it doesn't write an error to stdout if a service doesn't exist
+ Write-Host "Disabled UsoSvc Service"
+ Stop-Service -Name UsoSvc -Force
+ Set-Service -Name UsoSvc -StartupType Disabled
- Write-Host "Setting $service StartupType to Disabled"
- Get-Service -Name $service -ErrorAction SilentlyContinue | Set-Service -StartupType Disabled
+ Remove-Item "C:\Windows\SoftwareDistribution\*" -Recurse -Force
+ Write-Host "Cleared SoftwareDistribution folder"
+
+ Write-Host "Disabling update related scheduled tasks..." -ForegroundColor Yellow
+
+ $Tasks =
+ '\Microsoft\Windows\InstallService\*',
+ '\Microsoft\Windows\UpdateOrchestrator\*',
+ '\Microsoft\Windows\UpdateAssistant\*',
+ '\Microsoft\Windows\WaaSMedic\*',
+ '\Microsoft\Windows\WindowsUpdate\*',
+ '\Microsoft\WindowsUpdate\*'
+
+ foreach ($Task in $Tasks) {
+ Get-ScheduledTask -TaskPath $Task | Disable-ScheduledTask -ErrorAction SilentlyContinue
}
- Write-Host "================================="
- Write-Host "--- Updates ARE DISABLED ---"
- Write-Host "================================="
+
+ Write-Host "=================================" -ForegroundColor Green
+ Write-Host "--- Updates Are Disabled ---" -ForegroundColor Green
+ Write-Host "=================================" -ForegroundColor Green
+
+ Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
}
diff --git a/functions/public/Invoke-WPFUpdatessecurity.ps1 b/functions/public/Invoke-WPFUpdatessecurity.ps1
index ed7fe937cd..6306189583 100644
--- a/functions/public/Invoke-WPFUpdatessecurity.ps1
+++ b/functions/public/Invoke-WPFUpdatessecurity.ps1
@@ -12,42 +12,36 @@ function Invoke-WPFUpdatessecurity {
5. Defers quality updates for 4 days
#>
+
Write-Host "Disabling driver offering through Windows Update..."
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -Type DWord -Value 1
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontPromptForWindowsUpdate" -Type DWord -Value 1
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontSearchWindowsUpdate" -Type DWord -Value 1
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DriverUpdateWizardWuSearchEnabled" -Type DWord -Value 0
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
- Write-Host "Disabling Windows Update automatic restart..."
- If (!(Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU")) {
- New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUPowerManagement" -Type DWord -Value 0
- Write-Host "Disabled driver offering through Windows Update"
- If (!(Test-Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings")) {
- New-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Force | Out-Null
- }
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "BranchReadinessLevel" -Type DWord -Value 20
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferFeatureUpdatesPeriodInDays" -Type DWord -Value 365
- Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
-
- $ButtonType = [System.Windows.MessageBoxButton]::OK
- $MessageboxTitle = "Set Security Updates"
- $Messageboxbody = ("Recommended Update settings loaded")
- $MessageIcon = [System.Windows.MessageBoxImage]::Information
-
- [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
- Write-Host "================================="
- Write-Host "-- Updates Set to Recommended ---"
- Write-Host "================================="
+
+ New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Force
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -Type DWord -Value 1
+
+ New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Force
+
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontPromptForWindowsUpdate" -Type DWord -Value 1
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontSearchWindowsUpdate" -Type DWord -Value 1
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DriverUpdateWizardWuSearchEnabled" -Type DWord -Value 0
+
+ New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Force
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
+
+ Write-Host "Setting cumulative updates back by 1 year and security updates by 4 days"
+
+ New-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Force
+
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "BranchReadinessLevel" -Type DWord -Value 20
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferFeatureUpdatesPeriodInDays" -Type DWord -Value 365
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
+
+ Write-Host "Disabling Windows Update automatic restart..."
+
+ New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
+ Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUPowerManagement" -Type DWord -Value 0
+
+ Write-Host "================================="
+ Write-Host "-- Updates Set to Recommended ---"
+ Write-Host "================================="
}
diff --git a/functions/public/Invoke-WPFtweaksbutton.ps1 b/functions/public/Invoke-WPFtweaksbutton.ps1
index d0a5319086..192ca28411 100644
--- a/functions/public/Invoke-WPFtweaksbutton.ps1
+++ b/functions/public/Invoke-WPFtweaksbutton.ps1
@@ -12,11 +12,15 @@ function Invoke-WPFtweaksbutton {
return
}
- $Tweaks = (Get-WinUtilCheckBoxes)["WPFTweaks"]
+ $Tweaks = $sync.selectedTweaks
+ $dnsProvider = $sync["WPFchangedns"].text
+ $restorePointTweak = "WPFTweaksRestorePoint"
+ $restorePointSelected = $Tweaks -contains $restorePointTweak
+ $tweaksToRun = @($Tweaks | Where-Object { $_ -ne $restorePointTweak })
+ $totalSteps = [Math]::Max($Tweaks.Count, 1)
+ $completedSteps = 0
- Set-WinUtilDNS -DNSProvider $sync["WPFchangedns"].text
-
- if ($tweaks.count -eq 0 -and $sync["WPFchangedns"].text -eq "Default") {
+ if ($tweaks.count -eq 0 -and $dnsProvider -eq "Default") {
$msg = "Please check the tweaks you wish to perform."
[System.Windows.MessageBox]::Show($msg, "Winutil", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
return
@@ -24,39 +28,59 @@ function Invoke-WPFtweaksbutton {
Write-Debug "Number of tweaks to process: $($Tweaks.Count)"
- # The leading "," in the ParameterList is nessecary because we only provide one argument and powershell cannot be convinced that we want a nested loop with only one argument otherwise
- $tweaksHandle = Invoke-WPFRunspace -ParameterList @(,("tweaks",$tweaks)) -DebugPreference $DebugPreference -ScriptBlock {
- param(
- $tweaks,
- $DebugPreference
- )
+ if ($restorePointSelected) {
+ $sync.ProcessRunning = $true
+
+ if ($Tweaks.Count -eq 1) {
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
+ } else {
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
+ }
+
+ Set-WinUtilProgressBar -Label "Creating restore point" -Percent 0
+ Invoke-WinUtilTweaks $restorePointTweak
+ $completedSteps = 1
+
+ if ($tweaksToRun.Count -eq 0 -and $dnsProvider -eq "Default") {
+ Set-WinUtilProgressBar -Label "Tweaks finished" -Percent 100
+ $sync.ProcessRunning = $false
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
+ Write-Host "================================="
+ Write-Host "-- Tweaks are Finished ---"
+ Write-Host "================================="
+ return
+ }
+ }
+
+ # The leading "," in the ParameterList is necessary because we only provide one argument and powershell cannot be convinced that we want a nested loop with only one argument otherwise
+ $handle = Invoke-WPFRunspace -ParameterList @(("tweaks", $tweaksToRun), ("dnsProvider", $dnsProvider), ("completedSteps", $completedSteps), ("totalSteps", $totalSteps)) -ScriptBlock {
+ param($tweaks, $dnsProvider, $completedSteps, $totalSteps)
Write-Debug "Inside Number of tweaks to process: $($Tweaks.Count)"
$sync.ProcessRunning = $true
- if ($Tweaks.count -eq 1) {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" })
- } else {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" })
+ if ($completedSteps -eq 0) {
+ if ($Tweaks.count -eq 1) {
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
+ } else {
+ Invoke-WPFUIThread -ScriptBlock{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
+ }
}
- # Execute other selected tweaks
- for ($i = 0; $i -lt $Tweaks.Count; $i++) {
- Set-WinUtilProgressBar -Label "Applying $($tweaks[$i])" -Percent ($i / $tweaks.Count * 100)
+ Set-WinUtilDNS -DNSProvider $dnsProvider
+
+ for ($i = 0; $i -lt $tweaks.Count; $i++) {
+ Set-WinUtilProgressBar -Label "Applying $($tweaks[$i])" -Percent ($completedSteps / $totalSteps * 100)
Invoke-WinUtilTweaks $tweaks[$i]
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$Tweaks.Count) })
+ $completedSteps++
+ $progress = $completedSteps / $totalSteps
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value $progress }
}
Set-WinUtilProgressBar -Label "Tweaks finished" -Percent 100
$sync.ProcessRunning = $false
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "================================="
Write-Host "-- Tweaks are Finished ---"
Write-Host "================================="
-
- # $ButtonType = [System.Windows.MessageBoxButton]::OK
- # $MessageboxTitle = "Tweaks are Finished "
- # $Messageboxbody = ("Done")
- # $MessageIcon = [System.Windows.MessageBoxImage]::Information
- # [System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
}
}
diff --git a/functions/public/Invoke-WPFundoall.ps1 b/functions/public/Invoke-WPFundoall.ps1
index cb3ce7ba23..6bde6d1304 100644
--- a/functions/public/Invoke-WPFundoall.ps1
+++ b/functions/public/Invoke-WPFundoall.ps1
@@ -12,7 +12,7 @@ function Invoke-WPFundoall {
return
}
- $tweaks = (Get-WinUtilCheckBoxes)["WPFtweaks"]
+ $tweaks = $sync.selectedTweaks
if ($tweaks.count -eq 0) {
$msg = "Please check the tweaks you wish to undo."
@@ -20,26 +20,26 @@ function Invoke-WPFundoall {
return
}
- Invoke-WPFRunspace -ArgumentList $tweaks -DebugPreference $DebugPreference -ScriptBlock {
- param($tweaks, $DebugPreference)
+ Invoke-WPFRunspace -ArgumentList $tweaks -ScriptBlock {
+ param($tweaks)
$sync.ProcessRunning = $true
if ($tweaks.count -eq 1) {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
} else {
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
}
for ($i = 0; $i -lt $tweaks.Count; $i++) {
Set-WinUtilProgressBar -Label "Undoing $($tweaks[$i])" -Percent ($i / $tweaks.Count * 100)
Invoke-WinUtiltweaks $tweaks[$i] -undo $true
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -value ($i/$tweaks.Count) })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($i/$tweaks.Count) }
}
Set-WinUtilProgressBar -Label "Undo Tweaks Finished" -Percent 100
$sync.ProcessRunning = $false
- $sync.form.Dispatcher.Invoke([action]{ Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" })
+ Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "=================================="
Write-Host "--- Undo Tweaks are Finished ---"
Write-Host "=================================="
diff --git a/functions/public/Invoke-WinUtilAutoRun.ps1 b/functions/public/Invoke-WinUtilAutoRun.ps1
new file mode 100644
index 0000000000..022dae2263
--- /dev/null
+++ b/functions/public/Invoke-WinUtilAutoRun.ps1
@@ -0,0 +1,48 @@
+function Invoke-WinUtilAutoRun {
+ <#
+
+ .SYNOPSIS
+ Runs Install, Tweaks, and Features with optional UI invocation.
+ #>
+
+ function BusyWait {
+ Start-Sleep -Seconds 5
+ while ($sync.ProcessRunning) {
+ Start-Sleep -Seconds 5
+ }
+ }
+
+ BusyWait
+
+ Write-Host "Applying tweaks..."
+ Invoke-WPFtweaksbutton
+ BusyWait
+
+ Write-Host "Applying toggles..."
+ $handle = Invoke-WPFRunspace -ScriptBlock {
+ $Toggles = $sync.selectedToggles
+ Write-Debug "Inside Number of toggles to process: $($Toggles.Count)"
+
+ $sync.ProcessRunning = $true
+
+ for ($i = 0; $i -lt $Tweaks.Count; $i++) {
+ Invoke-WinUtilTweaks $Toggles[$i]
+ }
+
+ $sync.ProcessRunning = $false
+ Write-Host "================================="
+ Write-Host "-- Toggles are Finished ---"
+ Write-Host "================================="
+ }
+ BusyWait
+
+ Write-Host "Applying features..."
+ Invoke-WPFFeatureInstall
+ BusyWait
+
+ Write-Host "Installing applications..."
+ Invoke-WPFInstall
+ BusyWait
+
+ Write-Host "Done."
+}
diff --git a/functions/public/Invoke-WinUtilRemoveEdge.ps1 b/functions/public/Invoke-WinUtilRemoveEdge.ps1
new file mode 100644
index 0000000000..def31d7fc7
--- /dev/null
+++ b/functions/public/Invoke-WinUtilRemoveEdge.ps1
@@ -0,0 +1,8 @@
+function Invoke-WinUtilRemoveEdge {
+ $Path = Get-ChildItem -Path "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\*\Installer\setup.exe" | Select-Object -First 1
+
+ New-Item -Path "$Env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\MicrosoftEdge.exe" -Force
+ Start-Process -FilePath $Path -ArgumentList '--uninstall --system-level --force-uninstall --delete-profile' -Wait
+
+ Write-Host "Microsoft Edge was removed" -ForegroundColor Green
+}
diff --git a/functions/public/Show-CTTLogo.ps1 b/functions/public/Show-CTTLogo.ps1
new file mode 100644
index 0000000000..dea16fff20
--- /dev/null
+++ b/functions/public/Show-CTTLogo.ps1
@@ -0,0 +1,38 @@
+Function Show-CTTLogo {
+ <#
+ .SYNOPSIS
+ Displays the CTT logo in ASCII art.
+ .DESCRIPTION
+ This function displays the CTT logo in ASCII art format.
+ .PARAMETER None
+ No parameters are required for this function.
+ .EXAMPLE
+ Show-CTTLogo
+ Prints the CTT logo in ASCII art format to the console.
+ #>
+
+ $asciiArt = @"
+ CCCCCCCCCCCCCTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
+ CCC::::::::::::CT:::::::::::::::::::::TT:::::::::::::::::::::T
+CC:::::::::::::::CT:::::::::::::::::::::TT:::::::::::::::::::::T
+C:::::CCCCCCCC::::CT:::::TT:::::::TT:::::TT:::::TT:::::::TT:::::T
+C:::::C CCCCCCTTTTTT T:::::T TTTTTTTTTTTT T:::::T TTTTTT
+C:::::C T:::::T T:::::T
+C:::::C T:::::T T:::::T
+C:::::C T:::::T T:::::T
+C:::::C T:::::T T:::::T
+C:::::C T:::::T T:::::T
+C:::::C T:::::T T:::::T
+C:::::C CCCCCC T:::::T T:::::T
+C:::::CCCCCCCC::::C TT:::::::TT TT:::::::TT
+CC:::::::::::::::C T:::::::::T T:::::::::T
+CCC::::::::::::C T:::::::::T T:::::::::T
+ CCCCCCCCCCCCC TTTTTTTTTTT TTTTTTTTTTT
+
+====Chris Titus Tech=====
+=====Windows Toolbox=====
+"@
+
+ Write-Host $asciiArt
+}
+
diff --git a/overrides/main.html b/overrides/main.html
index c8d5a4138f..60fb8ea733 100644
--- a/overrides/main.html
+++ b/overrides/main.html
@@ -3,7 +3,6 @@
{% block header %}
{{ super() }}
- Announcement: We are currently not adding any applications to WinUtil and any apps that will be added through a PR will be declined by the maintainer.
Announcement: We are currently reworking the docs to use Hugo rather then mkdocs.
{% endblock %}
diff --git a/releases/oscdimg.exe b/releases/oscdimg.exe
deleted file mode 100644
index 3270677f84..0000000000
Binary files a/releases/oscdimg.exe and /dev/null differ
diff --git a/scripts/main.ps1 b/scripts/main.ps1
index 785f55e746..fecee8ebca 100644
--- a/scripts/main.ps1
+++ b/scripts/main.ps1
@@ -1,16 +1,31 @@
+# Create enums
+Add-Type @"
+public enum PackageManagers
+{
+ Winget,
+ Choco
+}
+"@
+
# SPDX-License-Identifier: MIT
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine
$maxthreads = [int]$env:NUMBER_OF_PROCESSORS
# Create a new session state for parsing variables into our runspace
$hashVars = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'sync',$sync,$Null
+$debugVar = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'DebugPreference',$DebugPreference,$Null
+$uiVar = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_NOUI',$PARAM_NOUI,$Null
+$offlineVar = New-object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_OFFLINE',$PARAM_OFFLINE,$Null
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
# Add the variable to the session state
$InitialSessionState.Variables.Add($hashVars)
+$InitialSessionState.Variables.Add($debugVar)
+$InitialSessionState.Variables.Add($uiVar)
+$InitialSessionState.Variables.Add($offlineVar)
# Get every private function and add them to the session state
-$functions = Get-ChildItem function:\ | Where-Object { $_.Name -imatch 'winutil|Microwin|WPF' }
+$functions = Get-ChildItem function:\ | Where-Object { $_.Name -imatch 'winutil|WPF' }
foreach ($function in $functions) {
$functionDefinition = Get-Content function:\$($function.name)
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $($function.name), $functionDefinition
@@ -46,6 +61,41 @@ class GenericException : Exception {
GenericException($Message) : base($Message) {}
}
+# Load the configuration files
+
+$sync.configs.applicationsHashtable = @{}
+$sync.configs.applications.PSObject.Properties | ForEach-Object {
+ $sync.configs.applicationsHashtable[$_.Name] = $_.Value
+}
+
+Set-Preferences
+
+if ($PARAM_NOUI) {
+ Show-CTTLogo
+ if ($PARAM_CONFIG -and -not [string]::IsNullOrWhiteSpace($PARAM_CONFIG)) {
+ Write-Host "Running config file tasks..."
+ Invoke-WPFImpex -type "import" -Config $PARAM_CONFIG
+ if ($PARAM_RUN) {
+ Invoke-WinUtilAutoRun
+ }
+ else {
+ Write-Host "Did you forget to add '--Run'?";
+ }
+ $sync.runspace.Dispose()
+ $sync.runspace.Close()
+ [System.GC]::Collect()
+ Stop-Transcript
+ exit 1
+ }
+ else {
+ Write-Host "Cannot automatically run without a config file provided."
+ $sync.runspace.Dispose()
+ $sync.runspace.Close()
+ [System.GC]::Collect()
+ Stop-Transcript
+ exit 1
+ }
+}
$inputXML = $inputXML -replace 'mc:Ignorable="d"', '' -replace "x:N", 'N' -replace '^ Set-WinUtilProgressbar
$sync["Form"].TaskbarItemInfo = New-Object System.Windows.Shell.TaskbarItemInfo
@@ -201,65 +264,52 @@ $sync["Form"].Add_Closing({
$sync.SearchBarClearButton.Add_Click({
$sync.SearchBar.Text = ""
$sync.SearchBarClearButton.Visibility = "Collapsed"
+
+ # Focus the search bar after clearing the text
+ $sync.SearchBar.Focus()
+ $sync.SearchBar.SelectAll()
})
# add some shortcuts for people that don't like clicking
$commonKeyEvents = {
+ # Prevent shortcuts from executing if a process is already running
if ($sync.ProcessRunning -eq $true) {
return
}
- if ($_.Key -eq "Escape") {
- $sync.SearchBar.SelectAll()
- $sync.SearchBar.Text = ""
- $sync.SearchBarClearButton.Visibility = "Collapsed"
- return
- }
-
- # don't ask, I know what I'm doing, just go...
- if (($_.Key -eq "Q" -and $_.KeyboardDevice.Modifiers -eq "Ctrl")) {
- $this.Close()
+ # Handle key presses of single keys
+ switch ($_.Key) {
+ "Escape" { $sync.SearchBar.Text = "" }
}
+ # Handle Alt key combinations for navigation
if ($_.KeyboardDevice.Modifiers -eq "Alt") {
- if ($_.SystemKey -eq "I") {
- Invoke-WPFButton "WPFTab1BT"
- }
- if ($_.SystemKey -eq "T") {
- Invoke-WPFButton "WPFTab2BT"
- }
- if ($_.SystemKey -eq "C") {
- Invoke-WPFButton "WPFTab3BT"
- }
- if ($_.SystemKey -eq "U") {
- Invoke-WPFButton "WPFTab4BT"
- }
- if ($_.SystemKey -eq "M") {
- Invoke-WPFButton "WPFTab5BT"
- }
- if ($_.SystemKey -eq "P") {
- Write-Host "Your Windows Product Key: $((Get-WmiObject -query 'select * from SoftwareLicensingService').OA3xOriginalProductKey)"
+ $keyEventArgs = $_
+ switch ($_.SystemKey) {
+ "I" { Invoke-WPFButton "WPFTab1BT"; $keyEventArgs.Handled = $true } # Navigate to Install tab and suppress Windows Warning Sound
+ "T" { Invoke-WPFButton "WPFTab2BT"; $keyEventArgs.Handled = $true } # Navigate to Tweaks tab
+ "C" { Invoke-WPFButton "WPFTab3BT"; $keyEventArgs.Handled = $true } # Navigate to Config tab
+ "U" { Invoke-WPFButton "WPFTab4BT"; $keyEventArgs.Handled = $true } # Navigate to Updates tab
+ "W" { Invoke-WPFButton "WPFTab5BT"; $keyEventArgs.Handled = $true } # Navigate to Win11ISO tab
}
}
- # shortcut for the filter box
- if ($_.Key -eq "F" -and $_.KeyboardDevice.Modifiers -eq "Ctrl") {
- if ($sync.SearchBar.Text -eq "Ctrl-F to filter") {
- $sync.SearchBar.SelectAll()
- $sync.SearchBar.Text = ""
+ # Handle Ctrl key combinations for specific actions
+ if ($_.KeyboardDevice.Modifiers -eq "Ctrl") {
+ switch ($_.Key) {
+ "F" { $sync.SearchBar.Focus() } # Focus on the search bar
+ "Q" { $this.Close() } # Close the application
}
- $sync.SearchBar.Focus()
}
}
-
$sync["Form"].Add_PreViewKeyDown($commonKeyEvents)
$sync["Form"].Add_MouseLeftButtonDown({
- Invoke-WPFPopup -Action "Hide" -Popups @("Settings", "Theme")
+ Invoke-WPFPopup -Action "Hide" -Popups @("Settings", "Theme", "FontScaling")
$sync["Form"].DragMove()
})
$sync["Form"].Add_MouseDoubleClick({
- if ($_.OriginalSource -is [System.Windows.Controls.Grid] -or
- $_.OriginalSource -is [System.Windows.Controls.StackPanel]) {
+ if ($_.OriginalSource.Name -eq "NavDockPanel" -or
+ $_.OriginalSource.Name -eq "GridBesideNavDockPanel") {
if ($sync["Form"].WindowState -eq [Windows.WindowState]::Normal) {
$sync["Form"].WindowState = [Windows.WindowState]::Maximized
}
@@ -271,59 +321,10 @@ $sync["Form"].Add_MouseDoubleClick({
$sync["Form"].Add_Deactivated({
Write-Debug "WinUtil lost focus"
- Invoke-WPFPopup -Action "Hide" -Popups @("Settings", "Theme")
+ Invoke-WPFPopup -Action "Hide" -Popups @("Settings", "Theme", "FontScaling")
})
$sync["Form"].Add_ContentRendered({
-
- try {
- [void][Window]
- } catch {
-Add-Type @"
- using System;
- using System.Runtime.InteropServices;
- public class Window {
- [DllImport("user32.dll")]
- public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
-
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
-
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
-
- [DllImport("user32.dll")]
- public static extern int GetSystemMetrics(int nIndex);
- };
- public struct RECT {
- public int Left; // x position of upper-left corner
- public int Top; // y position of upper-left corner
- public int Right; // x position of lower-right corner
- public int Bottom; // y position of lower-right corner
- }
-"@
- }
-
- foreach ($proc in (Get-Process).where{ $_.MainWindowTitle -and $_.MainWindowTitle -like "*titus*" }) {
- # Check if the process's MainWindowHandle is valid
- if ($proc.MainWindowHandle -ne [System.IntPtr]::Zero) {
- Write-Debug "MainWindowHandle: $($proc.Id) $($proc.MainWindowTitle) $($proc.MainWindowHandle)"
- $windowHandle = $proc.MainWindowHandle
- } else {
- Write-Warning "Process found, but no MainWindowHandle: $($proc.Id) $($proc.MainWindowTitle)"
-
- }
- }
-
- $rect = New-Object RECT
- [Window]::GetWindowRect($windowHandle, [ref]$rect)
- $width = $rect.Right - $rect.Left
- $height = $rect.Bottom - $rect.Top
-
- Write-Debug "UpperLeft:$($rect.Left),$($rect.Top) LowerBottom:$($rect.Right),$($rect.Bottom). Width:$($width) Height:$($height)"
-
# Load the Windows Forms assembly
Add-Type -AssemblyName System.Windows.Forms
$primaryScreen = [System.Windows.Forms.Screen]::PrimaryScreen
@@ -338,9 +339,12 @@ Add-Type @"
Write-Debug "Primary Monitor Height: $screenHeight pixels"
# Compare with the primary monitor size
- if ($width -gt $screenWidth -or $height -gt $screenHeight) {
+ if ($sync.Form.ActualWidth -gt $screenWidth -or $sync.Form.ActualHeight -gt $screenHeight) {
Write-Debug "The specified width and/or height is greater than the primary monitor size."
- [void][Window]::MoveWindow($windowHandle, 0, 0, $screenWidth, $screenHeight, $True)
+ $sync.Form.Left = 0
+ $sync.Form.Top = 0
+ $sync.Form.Width = $screenWidth
+ $sync.Form.Height = $screenHeight
} else {
Write-Debug "The specified width and height are within the primary monitor size limits."
}
@@ -348,139 +352,80 @@ Add-Type @"
Write-Debug "Unable to retrieve information about the primary monitor."
}
- Invoke-WPFTab "WPFTab1BT"
- $sync["Form"].Focus()
-
- # maybe this is not the best place to load and execute config file?
- # maybe community can help?
- if ($PARAM_CONFIG) {
- Invoke-WPFImpex -type "import" -Config $PARAM_CONFIG
- if ($PARAM_RUN) {
- while ($sync.ProcessRunning) {
- Start-Sleep -Seconds 5
- }
- Start-Sleep -Seconds 5
+ if ($PARAM_OFFLINE) {
+ # Show offline banner
+ $sync.WPFOfflineBanner.Visibility = [System.Windows.Visibility]::Visible
- Write-Host "Applying tweaks..."
- Invoke-WPFtweaksbutton
- while ($sync.ProcessRunning) {
- Start-Sleep -Seconds 5
- }
- Start-Sleep -Seconds 5
+ # Disable the install tab
+ $sync.WPFTab1BT.IsEnabled = $false
+ $sync.WPFTab1BT.Opacity = 0.5
+ $sync.WPFTab1BT.ToolTip = "Internet connection required for installing applications"
- Write-Host "Installing features..."
- Invoke-WPFFeatureInstall
- while ($sync.ProcessRunning) {
- Start-Sleep -Seconds 5
- }
+ # Disable install-related buttons
+ $sync.WPFInstall.IsEnabled = $false
+ $sync.WPFUninstall.IsEnabled = $false
+ $sync.WPFInstallUpgrade.IsEnabled = $false
+ $sync.WPFGetInstalled.IsEnabled = $false
- Start-Sleep -Seconds 5
- Write-Host "Installing applications..."
- while ($sync.ProcessRunning) {
- Start-Sleep -Seconds 1
- }
- Invoke-WPFInstall
- Start-Sleep -Seconds 5
+ # Show offline indicator
+ Write-Host "Offline mode detected - Install tab disabled" -ForegroundColor Yellow
- Write-Host "Done."
- }
+ # Optionally switch to a different tab if install tab was going to be default
+ Invoke-WPFTab "WPFTab2BT" # Switch to Tweaks tab instead
+ }
+ else {
+ # Online - ensure install tab is enabled
+ $sync.WPFTab1BT.IsEnabled = $true
+ $sync.WPFTab1BT.Opacity = 1.0
+ $sync.WPFTab1BT.ToolTip = $null
+ Invoke-WPFTab "WPFTab1BT" # Default to install tab
}
-})
+ $sync["Form"].Focus()
-# Add event handlers for the RadioButtons
-$sync["ISOdownloader"].add_Checked({
- $sync["ISORelease"].Visibility = [System.Windows.Visibility]::Visible
- $sync["ISOLanguage"].Visibility = [System.Windows.Visibility]::Visible
-})
+ if ($PARAM_CONFIG -and -not [string]::IsNullOrWhiteSpace($PARAM_CONFIG)) {
+ Write-Host "Running config file tasks..."
+ Invoke-WPFImpex -type "import" -Config $PARAM_CONFIG
+ if ($PARAM_RUN) {
+ Invoke-WinUtilAutoRun
+ }
+ }
-$sync["ISOmanual"].add_Checked({
- $sync["ISORelease"].Visibility = [System.Windows.Visibility]::Collapsed
- $sync["ISOLanguage"].Visibility = [System.Windows.Visibility]::Collapsed
})
-$sync["ISORelease"].Items.Add("24H2") | Out-Null
-$sync["ISORelease"].SelectedItem = "24H2"
-
-$sync["ISOLanguage"].Items.Add("System Language ($(Microwin-GetLangFromCulture -langName $((Get-Culture).Name)))") | Out-Null
-if ($currentCulture -ne "English International") {
- $sync["ISOLanguage"].Items.Add("English International") | Out-Null
-}
-if ($currentCulture -ne "English") {
- $sync["ISOLanguage"].Items.Add("English") | Out-Null
-}
-if ($sync["ISOLanguage"].Items.Count -eq 1) {
- $sync["ISOLanguage"].IsEnabled = $false
-}
-$sync["ISOLanguage"].SelectedIndex = 0
-
-
-# Load Checkboxes and Labels outside of the Filter function only once on startup for performance reasons
-$filter = Get-WinUtilVariables -Type CheckBox
-$CheckBoxes = ($sync.GetEnumerator()).where{ $psitem.Key -in $filter }
+# The SearchBarTimer is used to delay the search operation until the user has stopped typing for a short period
+# This prevents the ui from stuttering when the user types quickly as it dosnt need to update the ui for every keystroke
-$filter = Get-WinUtilVariables -Type Label
-$labels = @{}
-($sync.GetEnumerator()).where{$PSItem.Key -in $filter} | ForEach-Object {$labels[$_.Key] = $_.Value}
-
-$allCategories = $checkBoxes.Name | ForEach-Object {$sync.configs.applications.$_} | Select-Object -Unique -ExpandProperty category
+$searchBarTimer = New-Object System.Windows.Threading.DispatcherTimer
+$searchBarTimer.Interval = [TimeSpan]::FromMilliseconds(300)
+$searchBarTimer.IsEnabled = $false
+$searchBarTimer.add_Tick({
+ $searchBarTimer.Stop()
+ switch ($sync.currentTab) {
+ "Install" {
+ Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text
+ }
+ "Tweaks" {
+ Find-TweaksByNameOrDescription -SearchString $sync.SearchBar.Text
+ }
+ }
+})
$sync["SearchBar"].Add_TextChanged({
if ($sync.SearchBar.Text -ne "") {
$sync.SearchBarClearButton.Visibility = "Visible"
} else {
$sync.SearchBarClearButton.Visibility = "Collapsed"
}
-
- $activeApplications = @()
-
- $textToSearch = $sync.SearchBar.Text.ToLower()
-
- foreach ($CheckBox in $CheckBoxes) {
- # Skip if the checkbox is null, it doesn't have content or it is the prefer Choco checkbox
- if ($CheckBox -eq $null -or $CheckBox.Value -eq $null -or $CheckBox.Value.Content -eq $null -or $CheckBox.Name -eq "WPFpreferChocolatey") {
- continue
- }
-
- $checkBoxName = $CheckBox.Key
- $textBlockName = $checkBoxName + "Link"
-
- # Retrieve the corresponding text block based on the generated name
- $textBlock = $sync[$textBlockName]
-
- if ($CheckBox.Value.Content.ToString().ToLower().Contains($textToSearch)) {
- $CheckBox.Value.Visibility = "Visible"
- $activeApplications += $sync.configs.applications.$checkboxName
- # Set the corresponding text block visibility
- if ($textBlock -ne $null -and $textBlock -is [System.Windows.Controls.TextBlock]) {
- $textBlock.Visibility = "Visible"
- }
- } else {
- $CheckBox.Value.Visibility = "Collapsed"
- # Set the corresponding text block visibility
- if ($textBlock -ne $null -and $textBlock -is [System.Windows.Controls.TextBlock]) {
- $textBlock.Visibility = "Collapsed"
- }
- }
- }
-
- $activeCategories = $activeApplications | Select-Object -ExpandProperty category -Unique
-
- foreach ($category in $activeCategories) {
- $sync[$category].Visibility = "Visible"
- }
- if ($activeCategories) {
- $inactiveCategories = Compare-Object -ReferenceObject $allCategories -DifferenceObject $activeCategories -PassThru
- } else {
- $inactiveCategories = $allCategories
- }
- foreach ($category in $inactiveCategories) {
- $sync[$category].Visibility = "Collapsed"
+ if ($searchBarTimer.IsEnabled) {
+ $searchBarTimer.Stop()
}
+ $searchBarTimer.Start()
})
$sync["Form"].Add_Loaded({
param($e)
+ $sync.Form.MinWidth = "1000"
$sync["Form"].MaxWidth = [Double]::PositiveInfinity
$sync["Form"].MaxHeight = [Double]::PositiveInfinity
})
@@ -488,17 +433,9 @@ $sync["Form"].Add_Loaded({
$NavLogoPanel = $sync["Form"].FindName("NavLogoPanel")
$NavLogoPanel.Children.Add((Invoke-WinUtilAssets -Type "logo" -Size 25)) | Out-Null
-# Initialize the hashtable
-$winutildir = @{}
-
-# Set the path for the winutil directory
-$winutildir["path"] = "$env:LOCALAPPDATA\winutil\"
-[System.IO.Directory]::CreateDirectory($winutildir["path"]) | Out-Null
-
-$winutildir["logo.ico"] = $winutildir["path"] + "cttlogo.ico"
-if (Test-Path $winutildir["logo.ico"]) {
- $sync["logorender"] = $winutildir["logo.ico"]
+if (Test-Path "$winutildir\logo.ico") {
+ $sync["logorender"] = "$winutildir\logo.ico"
} else {
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
}
@@ -513,59 +450,56 @@ $sync["Form"].Add_Activated({
$sync["ThemeButton"].Add_Click({
Write-Debug "ThemeButton clicked"
- Invoke-WPFPopup -PopupActionTable @{ "Settings" = "Hide"; "Theme" = "Toggle" }
- $_.Handled = $false
+ Invoke-WPFPopup -PopupActionTable @{ "Settings" = "Hide"; "Theme" = "Toggle"; "FontScaling" = "Hide" }
})
$sync["AutoThemeMenuItem"].Add_Click({
Write-Debug "About clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Theme")
Invoke-WinutilThemeChange -theme "Auto"
- $_.Handled = $false
})
$sync["DarkThemeMenuItem"].Add_Click({
Write-Debug "Dark Theme clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Theme")
Invoke-WinutilThemeChange -theme "Dark"
- $_.Handled = $false
})
$sync["LightThemeMenuItem"].Add_Click({
Write-Debug "Light Theme clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Theme")
Invoke-WinutilThemeChange -theme "Light"
- $_.Handled = $false
})
-
$sync["SettingsButton"].Add_Click({
Write-Debug "SettingsButton clicked"
- Invoke-WPFPopup -PopupActionTable @{ "Settings" = "Toggle"; "Theme" = "Hide" }
- $_.Handled = $false
+ Invoke-WPFPopup -PopupActionTable @{ "Settings" = "Toggle"; "Theme" = "Hide"; "FontScaling" = "Hide" }
})
$sync["ImportMenuItem"].Add_Click({
Write-Debug "Import clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Settings")
Invoke-WPFImpex -type "import"
- $_.Handled = $false
})
$sync["ExportMenuItem"].Add_Click({
Write-Debug "Export clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Settings")
Invoke-WPFImpex -type "export"
- $_.Handled = $false
})
$sync["AboutMenuItem"].Add_Click({
Write-Debug "About clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Settings")
$authorInfo = @"
-Author : @christitustech
-Runspace : @DeveloperDurp
-MicroWin : @KonTy, @CodingWonders
+Author : @ChrisTitusTech
+UI : @MyDrift-user, @Marterich
+Runspace : @DeveloperDurp, @Marterich
GitHub : ChrisTitusTech/winutil
Version : $($sync.version)
"@
Show-CustomDialog -Title "About" -Message $authorInfo
})
+$sync["DocumentationMenuItem"].Add_Click({
+ Write-Debug "Documentation clicked"
+ Invoke-WPFPopup -Action "Hide" -Popups @("Settings")
+ Start-Process "https://winutil.christitus.com/"
+})
$sync["SponsorMenuItem"].Add_Click({
Write-Debug "Sponsors clicked"
Invoke-WPFPopup -Action "Hide" -Popups @("Settings")
@@ -585,5 +519,85 @@ $sync["SponsorMenuItem"].Add_Click({
Show-CustomDialog -Title "Sponsors" -Message $authorInfo -EnableScroll $true
})
+# Font Scaling Event Handlers
+$sync["FontScalingButton"].Add_Click({
+ Write-Debug "FontScalingButton clicked"
+ Invoke-WPFPopup -PopupActionTable @{ "Settings" = "Hide"; "Theme" = "Hide"; "FontScaling" = "Toggle" }
+})
+
+$sync["FontScalingSlider"].Add_ValueChanged({
+ param($slider)
+ $percentage = [math]::Round($slider.Value * 100)
+ $sync.FontScalingValue.Text = "$percentage%"
+})
+
+$sync["FontScalingResetButton"].Add_Click({
+ Write-Debug "FontScalingResetButton clicked"
+ $sync.FontScalingSlider.Value = 1.0
+ $sync.FontScalingValue.Text = "100%"
+})
+
+$sync["FontScalingApplyButton"].Add_Click({
+ Write-Debug "FontScalingApplyButton clicked"
+ $scaleFactor = $sync.FontScalingSlider.Value
+ Invoke-WinUtilFontScaling -ScaleFactor $scaleFactor
+ Invoke-WPFPopup -Action "Hide" -Popups @("FontScaling")
+})
+
+# ── Win11ISO Tab button handlers ──────────────────────────────────────────────
+
+$sync["WPFTab5BT"].Add_Click({
+ $sync["Form"].Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
+})
+
+$sync["WPFWin11ISOBrowseButton"].Add_Click({
+ Write-Debug "WPFWin11ISOBrowseButton clicked"
+ Invoke-WinUtilISOBrowse
+})
+
+$sync["WPFWin11ISODownloadLink"].Add_Click({
+ Write-Debug "WPFWin11ISODownloadLink clicked"
+ Start-Process "https://www.microsoft.com/software-download/windows11"
+})
+
+$sync["WPFWin11ISOMountButton"].Add_Click({
+ Write-Debug "WPFWin11ISOMountButton clicked"
+ Invoke-WinUtilISOMountAndVerify
+})
+
+$sync["WPFWin11ISOModifyButton"].Add_Click({
+ Write-Debug "WPFWin11ISOModifyButton clicked"
+ Invoke-WinUtilISOModify
+})
+
+$sync["WPFWin11ISOChooseISOButton"].Add_Click({
+ Write-Debug "WPFWin11ISOChooseISOButton clicked"
+ $sync["WPFWin11ISOOptionUSB"].Visibility = "Collapsed"
+ Invoke-WinUtilISOExport
+})
+
+$sync["WPFWin11ISOChooseUSBButton"].Add_Click({
+ Write-Debug "WPFWin11ISOChooseUSBButton clicked"
+ $sync["WPFWin11ISOOptionUSB"].Visibility = "Visible"
+ Invoke-WinUtilISORefreshUSBDrives
+})
+
+$sync["WPFWin11ISORefreshUSBButton"].Add_Click({
+ Write-Debug "WPFWin11ISORefreshUSBButton clicked"
+ Invoke-WinUtilISORefreshUSBDrives
+})
+
+$sync["WPFWin11ISOWriteUSBButton"].Add_Click({
+ Write-Debug "WPFWin11ISOWriteUSBButton clicked"
+ Invoke-WinUtilISOWriteUSB
+})
+
+$sync["WPFWin11ISOCleanResetButton"].Add_Click({
+ Write-Debug "WPFWin11ISOCleanResetButton clicked"
+ Invoke-WinUtilISOCleanAndReset
+})
+
+# ──────────────────────────────────────────────────────────────────────────────
+
$sync["Form"].ShowDialog() | out-null
Stop-Transcript
diff --git a/scripts/start.ps1 b/scripts/start.ps1
index c289709c49..44766a2223 100644
--- a/scripts/start.ps1
+++ b/scripts/start.ps1
@@ -7,16 +7,12 @@
#>
param (
- [switch]$Debug,
[string]$Config,
- [switch]$Run
+ [switch]$Run,
+ [switch]$Noui,
+ [switch]$Offline
)
-# Set DebugPreference based on the -Debug switch
-if ($Debug) {
- $DebugPreference = "Continue"
-}
-
if ($Config) {
$PARAM_CONFIG = $Config
}
@@ -24,20 +20,19 @@ if ($Config) {
$PARAM_RUN = $false
# Handle the -Run switch
if ($Run) {
- Write-Host "Running config file tasks..."
$PARAM_RUN = $true
}
-# Load DLLs
-Add-Type -AssemblyName PresentationFramework
-Add-Type -AssemblyName System.Windows.Forms
+$PARAM_NOUI = $false
+if ($Noui) {
+ $PARAM_NOUI = $true
+}
+
+$PARAM_OFFLINE = $false
+if ($Offline) {
+ $PARAM_OFFLINE = $true
+}
-# Variable to sync between runspaces
-$sync = [Hashtable]::Synchronized(@{})
-$sync.PSScriptRoot = $PSScriptRoot
-$sync.version = "#{replaceme}"
-$sync.configs = @{}
-$sync.ProcessRunning = $false
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Output "Winutil needs to be run as Administrator. Attempting to relaunch."
@@ -46,31 +41,61 @@ if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]:
$PSBoundParameters.GetEnumerator() | ForEach-Object {
$argList += if ($_.Value -is [switch] -and $_.Value) {
"-$($_.Key)"
+ } elseif ($_.Value -is [array]) {
+ "-$($_.Key) $($_.Value -join ',')"
} elseif ($_.Value) {
- "-$($_.Key) `"$($_.Value)`""
+ "-$($_.Key) '$($_.Value)'"
}
}
- $script = if ($MyInvocation.MyCommand.Path) {
- "& { & '$($MyInvocation.MyCommand.Path)' $argList }"
+ $script = if ($PSCommandPath) {
+ "& { & `'$($PSCommandPath)`' $($argList -join ' ') }"
} else {
- "iex '& { $(irm https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1) } $argList'"
+ "&([ScriptBlock]::Create((irm https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1))) $($argList -join ' ')"
}
- $powershellcmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
- $processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $powershellcmd }
+ $powershellCmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
+ $processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { "$powershellCmd" }
- Start-Process $processCmd -ArgumentList "$powershellcmd -ExecutionPolicy Bypass -NoProfile -Command $script" -Verb RunAs
+ if ($processCmd -eq "wt.exe") {
+ Start-Process $processCmd -ArgumentList "$powershellCmd -ExecutionPolicy Bypass -NoProfile -Command `"$script`"" -Verb RunAs
+ } else {
+ Start-Process $processCmd -ArgumentList "-ExecutionPolicy Bypass -NoProfile -Command `"$script`"" -Verb RunAs
+ }
break
}
+# Load DLLs
+Add-Type -AssemblyName PresentationFramework
+Add-Type -AssemblyName System.Windows.Forms
+
+# Variable to sync between runspaces
+$sync = [Hashtable]::Synchronized(@{})
+$sync.PSScriptRoot = $PSScriptRoot
+$sync.version = "#{replaceme}"
+$sync.configs = @{}
+$sync.Buttons = [System.Collections.Generic.List[PSObject]]::new()
+$sync.preferences = @{}
+$sync.ProcessRunning = $false
+$sync.selectedApps = [System.Collections.Generic.List[string]]::new()
+$sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
+$sync.selectedToggles = [System.Collections.Generic.List[string]]::new()
+$sync.selectedFeatures = [System.Collections.Generic.List[string]]::new()
+$sync.currentTab = "Install"
+$sync.selectedAppsStackPanel
+$sync.selectedAppsPopup
+
$dateTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
-$logdir = "$env:localappdata\winutil\logs"
-[System.IO.Directory]::CreateDirectory("$logdir") | Out-Null
+# Set the path for the winutil directory
+$winutildir = "$env:LocalAppData\winutil"
+New-Item $winutildir -ItemType Directory -Force | Out-Null
+
+$logdir = "$winutildir\logs"
+New-Item $logdir -ItemType Directory -Force | Out-Null
Start-Transcript -Path "$logdir\winutil_$dateTime.log" -Append -NoClobber | Out-Null
# Set PowerShell window title
-$Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Admin)"
+$Host.UI.RawUI.WindowTitle = "WinUtil (Admin)"
clear-host
diff --git a/sign.bat b/sign.bat
new file mode 100644
index 0000000000..134f1a3508
--- /dev/null
+++ b/sign.bat
@@ -0,0 +1 @@
+signtool.exe sign /td sha256 /tr http://timestamp.digicert.com /fd sha256 /n "CT Tech Group LLC" .\winutil.ps1
diff --git a/tools/Invoke-Preprocessing.ps1 b/tools/Invoke-Preprocessing.ps1
index 0569bc3565..0afa1a1b0d 100644
--- a/tools/Invoke-Preprocessing.ps1
+++ b/tools/Invoke-Preprocessing.ps1
@@ -3,15 +3,9 @@ function Invoke-Preprocessing {
.SYNOPSIS
A function that does Code Formatting using RegEx, useful when trying to force specific coding standard(s) to a project.
- .PARAMETER ThrowExceptionOnEmptyFilesList
- A switch which'll throw an exception upon not finding any files inside the provided 'WorkingDir'.
-
- .PARAMETER SkipExcludedFilesValidation
- A switch to stop file path validation on 'ExcludedFiles' list.
-
.PARAMETER ExcludedFiles
A list of file paths which're *relative to* 'WorkingDir' Folder, every item in the list can be pointing to File (doesn't end with '\') or Directory (ends with '\') or None-Existing File/Directory.
- By default, it checks if everyitem exists, and throws an exception if one or more are not found (None-Existing), if you want to skip this validation, please consider providing the '-SkipExcludedFilesValidation' switch to skip this check.
+ By default, it checks if everyitem exists, and throws an exception if one or more are not found (None-Existing).
.PARAMETER WorkingDir
The folder to search inside recursively for files which're going to be Preprocessed (Code Formatted), unless they're found in 'ExcludedFiles' List.
@@ -29,47 +23,35 @@ function Invoke-Preprocessing {
.EXAMPLE
Invoke-Preprocessing -WorkingDir "DRIVE:\Path\To\Folder\" -ExcludedFiles @('file.txt', '.\.git\', '*.png') -ProgressStatusMessage "Doing Preprocessing"
- Calls 'Invoke-Preprocessing' function using Named Paramters, with 'WorkingDir' (Mandatory Parameter) which's used as the base folder when searching for files recursively (using 'Get-ChildItem'), other two paramters are, in order from right to left, the Optional 'ExcludeFiles', which can be a path to a file, folder, or pattern-matched (like '*.png'), and the 'ProgressStatusMessage', which's used in Progress Bar.
+ Calls 'Invoke-Preprocessing' function using Named Parameters, with 'WorkingDir' (Mandatory Parameter) which's used as the base folder when searching for files recursively (using 'Get-ChildItem'), other two parameters are, in order from right to left, the Optional 'ExcludeFiles', which can be a path to a file, folder, or pattern-matched (like '*.png'), and the 'ProgressStatusMessage', which's used in Progress Bar.
.EXAMPLE
Invoke-Preprocessing -WorkingDir "DRIVE:\Path\To\Folder\" -ExcludedFiles @('file.txt', '.\.git\', '*.png') -ProgressStatusMessage "Doing Preprocessing" -ProgressActivity "Re-Formatting Code"
Same as Example No. 1, but uses 'ProgressActivity' which's used in Progress Bar.
- .EXAMPLE
- Invoke-Preprocessing -ThrowExceptionOnEmptyFilesList -WorkingDir "DRIVE:\Path\To\Folder\" -ExcludedFiles @('file.txt', '.\.git\', '*.png') -ProgressStatusMessage "Doing Preprocessing"
-
- Same as Example No. 1, but uses '-ThrowExceptionOnEmptyFilesList', which's an optional parameter that'll make 'Invoke-Preprocessing' throw an exception when no files are found in 'WorkingDir' (not including the ExcludedFiles, of course), useful when you want to double check your parameters & you're sure there's files to process in the 'WorkingDir'.
-
.EXAMPLE
Invoke-Preprocessing -Skip -WorkingDir "DRIVE:\Path\To\Folder\" -ExcludedFiles @('file.txt', '.\.git\', '*.png') -ProgressStatusMessage "Doing Preprocessing"
- Same as Example No. 1, but uses '-SkipExcludedFilesValidation', which'll skip the validation step for 'ExcludedFiles' list. This can be useful when 'ExcludedFiles' list is generated from another function, or from unreliable source (you can't guarantee every item in list is a valid path), but you want to silently continue through the function.
#>
param (
- [Parameter(position=0)]
- [switch]$SkipExcludedFilesValidation,
-
- [Parameter(position=1)]
- [switch]$ThrowExceptionOnEmptyFilesList,
-
- [Parameter(Mandatory, position=2)]
+ [Parameter(Mandatory, position=1)]
[ValidateScript({[System.IO.Path]::IsPathRooted($_)})]
[string]$WorkingDir,
- [Parameter(position=3)]
+ [Parameter(position=2)]
[string[]]$ExcludedFiles,
- [Parameter(Mandatory, position=4)]
+ [Parameter(Mandatory, position=3)]
[string]$ProgressStatusMessage,
- [Parameter(position=5)]
+ [Parameter(position=4)]
[string]$ProgressActivity = "Preprocessing"
)
if (-NOT (Test-Path -PathType Container -Path "$WorkingDir")) {
- throw "[Invoke-Preprocessing] Invalid Paramter Value for 'WorkingDir', passed value: '$WorkingDir'. Either the path is a File or Non-Existing/Invlid, please double check your code."
+ throw "[Invoke-Preprocessing] Invalid Parameter Value for 'WorkingDir', passed value: '$WorkingDir'. Either the path is a File or Non-Existing/Invlid, please double check your code."
}
$InternalExcludedFiles = [System.Collections.Generic.List[string]]::new($ExcludedFiles.Count)
@@ -77,11 +59,14 @@ function Invoke-Preprocessing {
$InternalExcludedFiles.Add($excludedFile) | Out-Null
}
- # Validate the ExcludedItems List before continuing on,
- # that's if there's a list in the first place, and '-SkipInternalExcludedFilesValidation' was not provided.
+ # Validate the ExcludedItems List before continuing on
if ($ExcludedFiles.Count -gt 0) {
ForEach ($excludedFile in $ExcludedFiles) {
$filePath = "$(($WorkingDir -replace ('\\$', '')) + '\' + ($excludedFile -replace ('\.\\', '')))"
+ # Only attempt to create the directory if the excludedFile ends with '\'
+ if ($excludedFile -match '\\$' -and -not (Test-Path "$filePath")) {
+ New-Item -Path "$filePath" -ItemType Directory -Force | Out-Null
+ }
$files = Get-ChildItem -Recurse -Path "$filePath" -File -Force
if ($files.Count -gt 0) {
ForEach ($file in $files) {
@@ -90,9 +75,6 @@ function Invoke-Preprocessing {
} else { $failedFilesList += "'$filePath', " }
}
$failedFilesList = $failedFilesList -replace (',\s*$', '')
- if ((-not $failedFilesList -eq "") -and (-not $SkipExcludedFilesValidation)) {
- throw "[Invoke-Preprocessing] One or more File Paths and/or File Patterns were not found, you can use '-SkipExcludedFilesValidation' switch to skip this check, the failed to validate are: $failedFilesList"
- }
}
# Get Files List
@@ -111,16 +93,47 @@ function Invoke-Preprocessing {
if ($index -ge 0) { $files.RemoveAt($index) }
}
- $numOfFiles = $files.Count
+ # Define a path to store the file hashes
+ $hashFilePath = Join-Path -Path $WorkingDir -ChildPath ".preprocessor_hashes.json"
- if ($numOfFiles -eq 0) {
- if ($ThrowExceptionOnEmptyFilesList) {
- throw "[Invoke-Preprocessing] Found 0 Files to Preprocess inside 'WorkingDir' Directory and '-ThrowExceptionOnEmptyFilesList' Switch is provided, value of 'WorkingDir': '$WorkingDir'."
- } else {
- return # Do an early return, there's nothing else to do
+ # Load existing hashes if the file exists
+ $existingHashes = @{}
+ if (Test-Path -Path $hashFilePath) {
+ # intentionally dosn't use ConvertFrom-Json -AsHashtable as it isn't supported on old powershell versions
+ $file_content = Get-Content -Path $hashFilePath | ConvertFrom-Json
+ foreach ($property in $file_content.PSObject.Properties) {
+ $existingHashes[$property.Name] = $property.Value
}
}
+ $newHashes = @{}
+ $changedFiles = @()
+ $hashingAlgorithm = "MD5"
+ foreach ($file in $files){
+ # Calculate the hash of the file
+ $hash = Get-FileHash -Path $file -Algorithm $hashingAlgorithm | Select-Object -ExpandProperty Hash
+ $newHashes[$file] = $hash
+
+ # Check if the hash already exists in the existing hashes
+ if (($existingHashes.ContainsKey($file) -and $existingHashes[$file] -eq $hash)) {
+ # Skip processing this file as it hasn't changed
+ continue;
+ }
+ else {
+ # If the hash doesn't exist or has changed, add it to the changed files list
+ $changedFiles += $file
+ }
+ }
+
+ $files = $changedFiles
+ $numOfFiles = $files.Count
+ Write-Debug "[Invoke-Preprocessing] Files Changed: $numOfFiles"
+
+ if ($numOfFiles -eq 0){
+ Write-Debug "[Invoke-Preprocessing] Found 0 Files to Preprocess inside 'WorkingDir' Directory : '$WorkingDir'."
+ return
+ }
+
for ($i = 0; $i -lt $numOfFiles; $i++) {
$fullFileName = $files[$i]
@@ -139,9 +152,13 @@ function Invoke-Preprocessing {
-replace ('\}\s*Catch\s*(?\[.*?\])\s*\{', '} catch ${exceptions} {') `
-replace ('(?\[[^$0-9]+\])\s*(?\$.*?)', '${parameter_type}${str_after_type}') `
| Set-Content "$fullFileName"
+ $newHashes[$fullFileName] = Get-FileHash -Path $fullFileName -Algorithm $hashingAlgorithm | Select-Object -ExpandProperty Hash
Write-Progress -Activity $ProgressActivity -Status "$ProgressStatusMessage - Finished $i out of $numOfFiles" -PercentComplete (($i/$numOfFiles)*100)
}
Write-Progress -Activity $ProgressActivity -Status "$ProgressStatusMessage - Finished Task Successfully" -Completed
+
+ # Save the new hashes to the file
+ $newHashes | ConvertTo-Json -Depth 10 | Set-Content -Path $hashFilePath
}
diff --git a/tools/autounattend.xml b/tools/autounattend.xml
new file mode 100644
index 0000000000..7a7c01e4b8
--- /dev/null
+++ b/tools/autounattend.xml
@@ -0,0 +1,497 @@
+
+
+
+
+
+
+
+ true
+
+ false
+
+
+ 1
+ reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassTPMCheck /t REG_DWORD /d 1 /f
+
+
+ 2
+ reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f
+
+
+ 3
+ reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f
+
+
+
+
+
+
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -NoProfile -Command "$xml = [xml]::new(); $xml.Load('C:\Windows\Panther\unattend.xml'); $sb = [scriptblock]::Create( $xml.unattend.Extensions.ExtractScript ); Invoke-Command -ScriptBlock $sb -ArgumentList $xml;"
+
+
+ 2
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\Specialize.ps1"
+
+
+ 3
+ reg.exe load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT"
+
+
+ 4
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\DefaultUser.ps1"
+
+
+ 5
+ reg.exe unload "HKU\DefaultUser"
+
+
+
+
+
+
+
+
+
+ 3
+ true
+ true
+ true
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\FirstLogon.ps1"
+
+
+
+
+
+
+param(
+ [xml]$Document
+);
+
+foreach( $file in $Document.unattend.Extensions.File ) {
+ $path = [System.Environment]::ExpandEnvironmentVariables( $file.GetAttribute( 'path' ) );
+ mkdir -Path( $path | Split-Path -Parent ) -ErrorAction 'SilentlyContinue';
+ $encoding = switch( [System.IO.Path]::GetExtension( $path ) ) {
+ { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; }
+ { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new( $false, $true ); }
+ default { [System.Text.Encoding]::Default; }
+ };
+ $bytes = $encoding.GetPreamble() + $encoding.GetBytes( $file.InnerText.Trim() );
+ [System.IO.File]::WriteAllBytes( $path, $bytes );
+}
+
+
+<LayoutModificationTemplate xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification" xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout" xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout" Version="1">
+ <CustomTaskbarLayoutCollection PinListPlacement="Replace">
+ <defaultlayout:TaskbarLayout>
+ <taskbar:TaskbarPinList>
+ <taskbar:DesktopApp DesktopApplicationLinkPath="#leaveempty" />
+ </taskbar:TaskbarPinList>
+ </defaultlayout:TaskbarLayout>
+ </CustomTaskbarLayoutCollection>
+</LayoutModificationTemplate>
+
+
+HKU = &H80000003
+Set reg = GetObject("winmgmts://./root/default:StdRegProv")
+Set fso = CreateObject("Scripting.FileSystemObject")
+
+If reg.EnumKey(HKU, "", sids) = 0 Then
+ If Not IsNull(sids) Then
+ For Each sid In sids
+ key = sid + "\Software\Policies\Microsoft\Windows\Explorer"
+ name = "LockedStartLayout"
+ If reg.GetDWORDValue(HKU, key, name, existing) = 0 Then
+ reg.SetDWORDValue HKU, key, name, 0
+ End If
+ Next
+ End If
+End If
+
+
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers>
+ <EventTrigger>
+ <Enabled>true</Enabled>
+ <Subscription><QueryList><Query Id="0" Path="Application"><Select Path="Application">*[System[Provider[@Name='UnattendGenerator'] and EventID=1]]</Select></Query></QueryList></Subscription>
+ </EventTrigger>
+ </Triggers>
+ <Principals>
+ <Principal id="Author">
+ <UserId>S-1-5-18</UserId>
+ <RunLevel>LeastPrivilege</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ <StartWhenAvailable>false</StartWhenAvailable>
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
+ <IdleSettings>
+ <StopOnIdleEnd>true</StopOnIdleEnd>
+ <RestartOnIdle>false</RestartOnIdle>
+ </IdleSettings>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <Enabled>true</Enabled>
+ <Hidden>false</Hidden>
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
+ <WakeToRun>false</WakeToRun>
+ <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
+ <Priority>7</Priority>
+ </Settings>
+ <Actions Context="Author">
+ <Exec>
+ <Command>C:\Windows\System32\wscript.exe</Command>
+ <Arguments>C:\Windows\Setup\Scripts\UnlockStartLayout.vbs</Arguments>
+ </Exec>
+ </Actions>
+</Task>
+
+
+$json = '{"pinnedList":[]}';
+if( [System.Environment]::OSVersion.Version.Build -lt 20000 ) {
+ return;
+}
+$key = 'Registry::HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start';
+New-Item -Path $key -ItemType 'Directory' -ErrorAction 'SilentlyContinue';
+Set-ItemProperty -LiteralPath $key -Name 'ConfigureStartPins' -Value $json -Type 'String';
+
+
+$lightThemeSystem = 0;
+$lightThemeApps = 0;
+$accentColorOnStart = 0;
+$enableTransparency = 0;
+$htmlAccentColor = '#0078D4';
+& {
+ $params = @{
+ LiteralPath = 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize';
+ Force = $true;
+ Type = 'DWord';
+ };
+ Set-ItemProperty @params -Name 'SystemUsesLightTheme' -Value $lightThemeSystem;
+ Set-ItemProperty @params -Name 'AppsUseLightTheme' -Value $lightThemeApps;
+ Set-ItemProperty @params -Name 'ColorPrevalence' -Value $accentColorOnStart;
+ Set-ItemProperty @params -Name 'EnableTransparency' -Value $enableTransparency;
+};
+& {
+ Add-Type -AssemblyName 'System.Drawing';
+ $accentColor = [System.Drawing.ColorTranslator]::FromHtml( $htmlAccentColor );
+
+ function ConvertTo-DWord {
+ param(
+ [System.Drawing.Color]
+ $Color
+ );
+
+ [byte[]]$bytes = @(
+ $Color.R;
+ $Color.G;
+ $Color.B;
+ $Color.A;
+ );
+ return [System.BitConverter]::ToUInt32( $bytes, 0);
+ }
+
+ $startColor = [System.Drawing.Color]::FromArgb( 0xD2, $accentColor );
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent' -Name 'StartColorMenu' -Value( ConvertTo-DWord -Color $accentColor ) -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent' -Name 'AccentColorMenu' -Value( ConvertTo-DWord -Color $accentColor ) -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\DWM' -Name 'AccentColor' -Value( ConvertTo-DWord -Color $accentColor ) -Type 'DWord' -Force;
+ $params = @{
+ LiteralPath = 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent';
+ Name = 'AccentPalette';
+ };
+ $palette = Get-ItemPropertyValue @params;
+ $index = 20;
+ $palette[ $index++ ] = $accentColor.R;
+ $palette[ $index++ ] = $accentColor.G;
+ $palette[ $index++ ] = $accentColor.B;
+ $palette[ $index++ ] = $accentColor.A;
+ Set-ItemProperty @params -Value $palette -Type 'Binary' -Force;
+};
+
+
+$scripts = @(
+ {
+ reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f;
+ };
+ {
+ net.exe accounts /maxpwage:UNLIMITED;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableCloudOptimizedContent" /t REG_DWORD /d 1 /f;
+ [System.Diagnostics.EventLog]::CreateEventSource( 'UnattendGenerator', 'Application' );
+ };
+ {
+ Register-ScheduledTask -TaskName 'UnlockStartLayout' -Xml $( Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\UnlockStartLayout.xml' -Raw );
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
+ };
+ {
+ Remove-Item -LiteralPath 'C:\Users\Public\Desktop\Microsoft Edge.lnk' -ErrorAction 'SilentlyContinue' -Verbose;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Dsh" /v AllowNewsAndInterests /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge" /v HideFirstRunExperience /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v BackgroundModeEnabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v StartupBoostEnabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetStartPins.ps1';
+ };
+ {
+ reg.exe add "HKU\.DEFAULT\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /t REG_DWORD /d 1 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f;
+ };
+);
+
+& {
+ [float]$complete = 0;
+ [float]$increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to customize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $str = $script.ToString().Trim() -replace '\s+', ' ';
+ $max = 100;
+ if( $str.Length -le $max ) {
+ $str;
+ } else {
+ $str.Substring( 0, $max - 1 ) + '…';
+ }
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\Specialize.log";
+
+
+$scripts = @(
+ {
+ [System.Diagnostics.EventLog]::WriteEntry( 'UnattendGenerator', "User '$env:USERNAME' has requested to unlock the Start menu layout.", [System.Diagnostics.EventLogEntryType]::Information, 1 );
+ };
+ {
+ Remove-Item -Path "${env:USERPROFILE}\Desktop\*.lnk" -Force -ErrorAction 'SilentlyContinue';
+ Remove-Item -Path "$env:HOMEDRIVE\Users\Default\Desktop\*.lnk" -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ $taskbarPath = "$env:AppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar";
+ if( Test-Path $taskbarPath ) {
+ Get-ChildItem -Path $taskbarPath -File | Remove-Item -Force;
+ }
+ Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband' -Name 'FavoritesRemovedChanges' -Force -ErrorAction 'SilentlyContinue';
+ Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband' -Name 'FavoritesChanges' -Force -ErrorAction 'SilentlyContinue';
+ Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband' -Name 'Favorites' -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /ve /f;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name 'LaunchTo' -Type 'DWord' -Value 1;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Search' -Name 'SearchboxTaskbarMode' -Type 'DWord' -Value 0;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetColorTheme.ps1';
+ };
+ {
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.Suggested" /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.Suggested" /v Enabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.StartupApp" /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.StartupApp" /v Enabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.SkyDrive.Desktop" /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.SkyDrive.Desktop" /v Enabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.AccountHealth" /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.AccountHealth" /v Enabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Start" /v AllAppsViewMode /t REG_DWORD /d 2 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Start_IrisRecommendations /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Start_AccountNotifications /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Start" /v ShowAllPinsList /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Start" /v ShowFrequentList /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Start" /v ShowRecentList /t REG_DWORD /d 0 /f;
+ reg.exe add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Start_TrackDocs /t REG_DWORD /d 0 /f;
+ };
+ {
+ Restart-Computer -Force;
+ };
+);
+
+& {
+ [float]$complete = 0;
+ [float]$increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to configure this user account. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $str = $script.ToString().Trim() -replace '\s+', ' ';
+ $max = 100;
+ if( $str.Length -le $max ) {
+ $str;
+ } else {
+ $str.Substring( 0, $max - 1 ) + '…';
+ }
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "$env:TEMP\UserOnce.log";
+
+
+$scripts = @(
+ {
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\Explorer" /v "StartLayoutFile" /t REG_SZ /d "C:\Windows\Setup\Scripts\TaskbarLayoutModification.xml" /f;
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\Explorer" /v "LockedStartLayout" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v ShowTaskViewButton /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v TaskbarAl /t REG_DWORD /d 0 /f;
+ };
+ {
+ foreach( $root in 'Registry::HKU\.DEFAULT', 'Registry::HKU\DefaultUser' ) {
+ Set-ItemProperty -LiteralPath "$root\Control Panel\Keyboard" -Name 'InitialKeyboardIndicators' -Type 'String' -Value 2 -Force;
+ }
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarDeveloperSettings" /v TaskbarEndTask /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\DWM" /v ColorPrevalence /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v "UnattendedSetup" /t REG_SZ /d "powershell.exe -WindowStyle \""Normal\"" -ExecutionPolicy \""Unrestricted\"" -NoProfile -File \""C:\Windows\Setup\Scripts\UserOnce.ps1\""" /f;
+ };
+);
+
+& {
+ [float]$complete = 0;
+ [float]$increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to modify the default user’’s registry hive. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $str = $script.ToString().Trim() -replace '\s+', ' ';
+ $max = 100;
+ if( $str.Length -le $max ) {
+ $str;
+ } else {
+ $str.Substring( 0, $max - 1 ) + '…';
+ }
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\DefaultUser.log";
+
+
+$scripts = @(
+ {
+ Remove-Item -LiteralPath @(
+ 'C:\Windows\Panther\unattend.xml';
+ 'C:\Windows\Panther\unattend-original.xml';
+ 'C:\Windows\Setup\Scripts\Wifi.xml';
+ 'C:\Windows.old';
+ ) -Recurse -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ reg.exe delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v OneDriveSetup /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v AUOptions /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v UseWUServer /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v DisableWindowsUpdateAccess /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v WUServer /f;
+ reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /v WUStatusServer /f;
+ reg.exe delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" /v DODownloadMode /f;
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\BITS" /v Start /t REG_DWORD /d 3 /f;
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\wuauserv" /v Start /t REG_DWORD /d 3 /f;
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\UsoSvc" /v Start /t REG_DWORD /d 2 /f;
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc" /v Start /t REG_DWORD /d 3 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Education" /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start" /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Education" /v IsEducationEnvironment /t REG_DWORD /d 1 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" /v HideRecommendedSection /t REG_DWORD /d 1 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start" /v HideRecommendedSection /t REG_DWORD /d 1 /f;
+ };
+ {
+ $recallFeature = Get-WindowsOptionalFeature -Online -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Enabled' -and $_.FeatureName -like 'Recall' };
+ if( $recallFeature ) {
+ Disable-WindowsOptionalFeature -Online -FeatureName 'Recall' -Remove -ErrorAction SilentlyContinue;
+ }
+ };
+ {
+ $viveDir = Join-Path $env:TEMP 'ViVeTool';
+ $viveZip = Join-Path $env:TEMP 'ViVeTool.zip';
+ Invoke-WebRequest 'https://github.com/thebookisclosed/ViVe/releases/download/v0.3.4/ViVeTool-v0.3.4-IntelAmd.zip' -OutFile $viveZip;
+ Expand-Archive -Path $viveZip -DestinationPath $viveDir -Force;
+ Remove-Item -Path $viveZip -Force;
+ Start-Process -FilePath (Join-Path $viveDir 'ViVeTool.exe') -ArgumentList '/disable /id:47205210' -Wait -NoNewWindow;
+ Remove-Item -Path $viveDir -Recurse -Force;
+ };
+ {
+ if( (Get-BitLockerVolume -MountPoint $Env:SystemDrive).ProtectionStatus -eq 'On' ) {
+ Disable-BitLocker -MountPoint $Env:SystemDrive;
+ }
+ };
+ {
+ if( (bcdedit | Select-String 'path').Count -eq 2 ) {
+ bcdedit /set `{bootmgr`} timeout 0;
+ }
+ };
+);
+
+& {
+ [float]$complete = 0;
+ [float]$increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to finalize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $str = $script.ToString().Trim() -replace '\s+', ' ';
+ $max = 100;
+ if( $str.Length -le $max ) {
+ $str;
+ } else {
+ $str.Substring( 0, $max - 1 ) + '…';
+ }
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\FirstLogon.log";
+
+
+
diff --git a/tools/devdocs-generator.md b/tools/devdocs-generator.md
new file mode 100644
index 0000000000..41d56d3be7
--- /dev/null
+++ b/tools/devdocs-generator.md
@@ -0,0 +1,105 @@
+---
+title: "Dev Docs Generator"
+description: "How the devdocs-generator.ps1 script works"
+---
+
+# Dev Docs Generator
+
+The `devdocs-generator.ps1` script automatically generates Hugo-compatible markdown files for the development documentation. It pulls content directly from the JSON config files and PowerShell function files so the docs never go out of sync.
+
+## When Does it Run?
+
+- Automatically triggered by the `docs.yaml` GitHub Actions workflow, which generates the `.md` files, commits them back to the repo, and then triggers Hugo to build the site
+- Automatically runs during the pre-release workflow, committing the updated `"link"` properties back to the JSON config files
+- Watches `docs/**`, `config/tweaks.json`, `config/feature.json`, and `functions/**` for changes
+- Supports manual runs via `workflow_dispatch`
+
+## What Does It Do?
+
+### 1. Loads the Data
+
+- Reads `config/tweaks.json` and `config/feature.json`
+- Reads all `.ps1` function files from `functions/public/` and `functions/private/`
+- Parses `Invoke-WPFButton.ps1` to build a mapping of button names to their function names
+
+### 2. Updates Links in JSON
+
+- Adds or updates a `"link"` property on every entry in both JSON config files
+- Each link points to that entry's documentation page on the Hugo site
+- The updated links are automatically committed back to the JSON config files as part of the pre-release workflow
+
+### 3. Cleans Up Old Docs
+
+- Deletes all `.md` files (except `_index.md`) from `docs/content/dev/tweaks/` and `docs/content/dev/features/`
+- This prevents duplicate or orphaned files from previous runs
+
+### 4. Generates Tweak Documentation
+
+For each entry in `tweaks.json` that belongs to a documented category:
+
+- **Button type** entries get the mapped PowerShell function file embedded
+- **All other types** get the raw JSON snippet embedded with correct line numbers from the source file
+- Entries with **registry changes** get a Registry Changes section added
+- Entries with **services** get the `Set-WinUtilService.ps1` function appended
+
+### 5. Generates Feature Documentation
+
+For each entry in `feature.json` that belongs to a documented category:
+
+- **Fixes and Legacy Windows Panels** get the mapped PowerShell function file embedded
+- **Features** get the raw JSON snippet embedded with correct line numbers
+
+### 6. Output Format
+
+- Every `.md` file gets Hugo frontmatter with `title` and `description`
+- Code blocks use Hugo syntax with filename labels and line numbers
+- Files are organized into category subdirectories matching the JSON `category` field
+
+## Documented Categories
+
+The script generates docs for entries in these categories:
+
+- Essential Tweaks
+- z--Advanced-Tweaks---CAUTION
+- Customize Preferences
+- Performance Plans
+- Features
+- Fixes
+- Legacy Windows Panels
+
+## File Structure
+
+```
+docs/content/dev/
+ tweaks/
+ Essential-Tweaks/
+ z--Advanced-Tweaks---CAUTION/
+ Customize-Preferences/
+ Performance-Plans/
+ features/
+ Features/
+ Fixes/
+ Legacy-Windows-Panels/
+```
+
+## How File Names Are Derived
+
+The script strips common prefixes from the JSON key names using the pattern `WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?`. For example:
+
+| JSON Key | Generated File |
+| ------------------- | -------------- |
+| `WPFTweaksHiber` | `Hiber.md` |
+| `WPFTweaksDeBloat` | `DeBloat.md` |
+| `WPFFeatureshyperv` | `hyperv.md` |
+| `WPFPanelDISM` | `DISM.md` |
+
+## Key Points
+
+- The JSON config files are the single source of truth
+- Manual edits to generated `.md` files will be overwritten on the next run
+- The script does not modify `_index.md` or `architecture.md`
+ — do not delete `_index.md` or `architecture.md`, as they will need to be recreated manually.
+- Category directories are created automatically if they don't exist
+- The `"link"` property added to JSON entries is excluded from the displayed code blocks
+- The `docs` workflow generates the `.md` files and commits them back to the repo before Hugo builds the site
+- The `pre-release` workflow generates the `"link"` properties and commits them back to the repo
diff --git a/tools/devdocs-generator.ps1 b/tools/devdocs-generator.ps1
index 792f6581af..2853b36e16 100644
--- a/tools/devdocs-generator.ps1
+++ b/tools/devdocs-generator.ps1
@@ -1,644 +1,343 @@
<#
.DESCRIPTION
- This script generates markdown files for the development documentation based on the existing JSON files.
- Create table of content and archive any files in the dev folder not modified by this script.
- This script is not meant to be used manually, it is called by the github action workflow.
+ Generates Hugo markdown docs from config/tweaks.json and config/feature.json.
+ Run by the GitHub Actions docs workflow before Hugo build.
#>
-function Process-MultilineStrings {
- param (
- [Parameter(Mandatory, position=0)]
- [string]$str
- )
-
- $lines = $str.Split("`r`n")
- $count = $lines.Count
-
- # Loop through every line, expect last line in the string
- # We'll add it after the for loop
- for ($i = 0; $i -lt ($count - 1); $i++) {
- $line = $lines[$i]
- $processedStr += $line -replace ('^\s*\\\\', '')
- # Add the previously removed NewLine character by 'Split' Method
- $processedStr += "`r`n"
- }
-
- # Add last line *without* a NewLine character.
- $processedStr += $lines[$($count - 1)] -replace ('^\s*\\\\', '')
-
- return $processedStr
-}
-
function Update-Progress {
param (
[Parameter(Mandatory, position=0)]
[string]$StatusMessage,
-
[Parameter(Mandatory, position=1)]
[ValidateRange(0,100)]
- [int]$Percent,
-
- [Parameter(position=2)]
- [string]$Activity = "Compiling"
+ [int]$Percent
)
-
- Write-Progress -Activity $Activity -Status $StatusMessage -PercentComplete $Percent
+ Write-Progress -Activity "Generating Dev Docs" -Status $StatusMessage -PercentComplete $Percent
}
-function Load-Functions {
+function Get-RawJsonBlock {
+ # Returns the raw JSON text and 1-based start line for an item, excluding the "link" property.
param (
- [Parameter(Mandatory, position=0)]
- [string]$dir
+ [Parameter(Mandatory)]
+ [string]$ItemName,
+ [Parameter(Mandatory)]
+ [AllowEmptyString()]
+ [string[]]$JsonLines
)
- Get-ChildItem -Path $dir -Filter *.ps1 | ForEach-Object {
- $functionName = $_.BaseName
- $functionContent = Get-Content -Path $_.FullName -Raw
- $functions[$functionName] = $functionContent
- }
-}
-
-function Get-CalledFunctions {
- param (
- [Parameter(Mandatory, position=0)]
- $scriptContent,
-
- [Parameter(Mandatory, position=1)]
- [hashtable]$functionList,
-
- [Parameter(Mandatory, position=2)]
- [ref]$processedFunctions
- )
+ $escapedName = [regex]::Escape($ItemName)
+ $startIndex = -1
+ $startIndent = ""
- $calledFunctions = @()
- foreach ($functionName in $functionList.Keys) {
- if ($scriptContent -match "\b$functionName\b" -and -not $processedFunctions.Value.Contains($functionName)) {
- $calledFunctions += $functionName
- $processedFunctions.Value.Add($functionName)
- if ($functionList[$functionName]) {
- $nestedFunctions = Get-CalledFunctions -scriptContent $functionList[$functionName] -functionList $functionList -processedFunctions $processedFunctions
- $calledFunctions += $nestedFunctions
- }
+ for ($i = 0; $i -lt $JsonLines.Count; $i++) {
+ if ($JsonLines[$i] -match "^(\s*)`"$escapedName`"\s*:\s*\{") {
+ $startIndex = $i
+ $startIndent = $matches[1]
+ break
}
}
- return $calledFunctions
-}
-function Get-AdditionalFunctionsFromToggle {
- param (
- [Parameter(Mandatory, position=0)]
- [string]$buttonName
- )
+ if ($startIndex -eq -1) {
+ Write-Warning "Could not find '$ItemName' in JSON"
+ return $null
+ }
- $invokeWpfToggleContent = Get-Content -Path "$publicFunctionsDir/Invoke-WPFToggle.ps1" -Raw
- $lines = $invokeWpfToggleContent -split "`r`n"
- foreach ($line in $lines) {
- if ($line -match "`"$buttonName`" \{Invoke-(WinUtil[a-zA-Z]+)") {
- return $matches[1]
+ $escapedIndent = [regex]::Escape($startIndent)
+ $endIndex = -1
+ for ($i = ($startIndex + 1); $i -lt $JsonLines.Count; $i++) {
+ if ($JsonLines[$i] -match "^$escapedIndent\}") {
+ $endIndex = $i
+ break
}
}
-}
-function Get-AdditionalFunctionsFromButton {
- param (
- [Parameter(Mandatory, position=0)]
- [string]$buttonName
- )
+ if ($endIndex -eq -1) {
+ Write-Warning "Could not find closing brace for '$ItemName'"
+ return $null
+ }
- $invokeWpfButtonContent = Get-Content -Path "$publicFunctionsDir/Invoke-WPFButton.ps1" -Raw
- $lines = $invokeWpfButtonContent -split "`r`n"
- foreach ($line in $lines) {
- if ($line -match "`"$buttonName`" \{Invoke-(WPF[a-zA-Z]+)") {
- return $matches[1]
+ # Strip trailing "link" property and blank lines before returning
+ $lastContentIndex = $endIndex - 1
+ while ($lastContentIndex -gt $startIndex) {
+ $trimmed = $JsonLines[$lastContentIndex].Trim()
+ if ($trimmed -eq "" -or $trimmed -match '^"link"') {
+ $lastContentIndex--
+ } else {
+ break
}
}
+
+ return @{
+ LineNumber = $startIndex + 1
+ RawText = ($JsonLines[$startIndex..$lastContentIndex] -join "`r`n")
+ }
}
-function Add-LinkAttribute {
+function Get-ButtonFunctionMapping {
+ # Parses Invoke-WPFButton.ps1 and returns a hashtable of button name -> function name.
param (
[Parameter(Mandatory)]
- [PSCustomObject]$jsonObject
+ [string]$ButtonFilePath
)
- $totalProperties = ($jsonObject.PSObject.Properties | Measure-Object).Count
- $progressIncrement = 50 / $totalProperties
- $currentProgress = 50
-
- foreach ($property in $jsonObject.PSObject.Properties) {
- if ($property.Value -is [PSCustomObject]) {
- Add-LinkAttribute -jsonObject $property.Value
- } elseif ($property.Value -is [System.Collections.ArrayList]) {
- foreach ($item in $property.Value) {
- if ($item -is [PSCustomObject]) {
- Add-LinkAttribute -jsonObject $item
- }
- }
+ $mapping = @{}
+ foreach ($line in (Get-Content -Path $ButtonFilePath)) {
+ if ($line -match '^\s*"(\w+)"\s*\{(Invoke-\w+)') {
+ $mapping[$matches[1]] = $matches[2]
}
- $currentProgress += $progressIncrement
- $roundedProgress = [math]::Round($currentProgress)
- Update-Progress -StatusMessage "Adding documentation links" -Percent $roundedProgress
- }
- if ($jsonObject -ne $global:rootObject) {
- $jsonObject | Add-Member -NotePropertyName "link" -NotePropertyValue "" -Force
}
+ return $mapping
}
-function Generate-MarkdownFiles {
+function Add-LinkAttributeToJson {
+ # Updates only the "link" property for each entry in a JSON config file.
+ # Reads via ConvertFrom-Json for metadata, then edits lines directly to avoid reformatting.
param (
- [Parameter(Mandatory, position=0)]
- [PSCustomObject]$data,
-
- [Parameter(Mandatory, position=1)]
- [string]$outputDir,
-
- [Parameter(Mandatory, position=2)]
- [string]$jsonFilePath,
-
- [Parameter(Mandatory, position=3)]
- [string]$lastModified,
-
- [Parameter(Mandatory, position=4)]
- [string]$type,
-
- [Parameter(position=5)]
- [int]$initialProgress
+ [Parameter(Mandatory)]
+ [string]$JsonFilePath,
+ [Parameter(Mandatory)]
+ [string]$UrlPrefix,
+ [Parameter(Mandatory)]
+ [string]$ItemNameToCut
)
- # TODO: Make the function reference generation better by making a Graph, so it highlights
- # Which function "depends" on which, and makes it clearer on a high-level for the reader
- # to understand the general structure.
-
- $totalItems = ($data.PSObject.Properties | Measure-Object).Count
- $progressIncrement = 10 / $totalItems
- $currentProgress = [int]$initialProgress
-
- $tocEntries = @()
- $processedFiles = @()
- foreach ($itemName in $data.PSObject.Properties.Name) {
- # Create Category Directory if needed.
- $itemDetails = $data.$itemName
- $category = $itemDetails.category -replace '[^a-zA-Z0-9]', '-'
- $categoryDir = "$outputDir/$category"
- if (-Not (Test-Path -Path $categoryDir)) {
- New-Item -ItemType Directory -Path $categoryDir | Out-Null
- }
+ $jsonData = Get-Content -Path $JsonFilePath -Raw | ConvertFrom-Json
+ $lines = [System.Collections.Generic.List[string]](Get-Content -Path $JsonFilePath)
- # Create empty files with correct path
- $fullItemName = $itemName
- $displayName = $itemName -replace $itemnametocut, ''
- $filename = "$categoryDir/$displayName.md"
- $relativePath = "$outputDir/$category/$displayName.md" -replace '^docs/', ''
- if (-Not (Test-Path -Path $filename)) {
- Set-Content -Path $filename -Value "" -Encoding utf8
- }
-
- # Add the entry to 'tocEntries' so we can generate Table Of Content easily
- # And add the Full FileName of entry
- $tocEntries += @{
- Category = $category
- Path = $relativePath
- Name = $itemDetails.Content
- Type = $type
+ foreach ($item in $jsonData.PSObject.Properties) {
+ $itemName = $item.Name
+ $category = $item.Value.category -replace '[^a-zA-Z0-9]', '-'
+ $displayName = $itemName -replace $ItemNameToCut, ''
+ $newLink = "$UrlPrefix/$($category.ToLower())/$($displayName.ToLower())"
+ $escapedName = [regex]::Escape($itemName)
+
+ # Find item start line
+ $startIdx = -1
+ for ($i = 0; $i -lt $lines.Count; $i++) {
+ if ($lines[$i] -match "^\s*`"$escapedName`"\s*:\s*\{") {
+ $startIdx = $i
+ break
+ }
}
- $processedFiles += (Get-Item $filename).FullName
-
- $header = "# $([string]$itemDetails.Content)" + "`r`n"
- $lastUpdatedNotice = "Last Updated: $lastModified" + "`r`n"
- $autoupdatenotice = Process-MultilineStrings @"
- \\!!! info
- \\ The Development Documentation is auto generated for every compilation of WinUtil, meaning a part of it will always stay up-to-date. **Developers do have the ability to add custom content, which won't be updated automatically.**
-"@
-
- $description = Process-MultilineStrings @"
- \\## Description
- \\
- \\$([string]$itemDetails.Description)
-"@
-
- $jsonContent = ($itemDetails | ConvertTo-Json -Depth 10).replace('\n',"`n").replace('\r', "`r")
- $codeBlock = Process-MultilineStrings @"
- \\
- \\Preview Code
- \\
- \\``````json
- \\$jsonContent
- \\``````
- \\
- \\
-"@
-
- # Clear the variable before continuing, will cause problems otherwise
- $FeaturesDocs = ""
- if ($itemDetails.feature) {
- $FeaturesDocs += Process-MultilineStrings @"
- \\## Features
- \\
- \\
- \\Optional Windows Features are additional functionalities or components in the Windows operating system that users can choose to enable or disable based on their specific needs and preferences.
- \\
- \\
- \\You can find information about Optional Windows Features on [Microsoft's Website for Optional Features](https://learn.microsoft.com/en-us/windows/client-management/client-tools/add-remove-hide-features?pivots=windows-11).
- \\
- \\
-"@
- if (($itemDetails.feature).Count -gt 1) {
- $FeaturesDocs += "### Features to install" + "`r`n"
- } else {
- $FeaturesDocs += "### Feature to install" + "`r`n"
+ if ($startIdx -eq -1) { continue }
+
+ # Derive indentation: propIndent is one level deeper than the item start.
+ # Used to target only top-level properties and skip nested object braces.
+ $null = $lines[$startIdx] -match '^(\s*)'
+ $propIndent = $matches[1] + ' '
+ $propIndentLen = $propIndent.Length
+ $escapedPropIndent = [regex]::Escape($propIndent)
+
+ # Scan forward: update existing "link" or find the closing brace to insert one.
+ # Closing brace is matched by indent <= propIndentLen to handle inconsistent formatting.
+ $linkUpdated = $false
+ $closeBraceIdx = -1
+ for ($j = $startIdx + 1; $j -lt $lines.Count; $j++) {
+ if ($lines[$j] -match "^$escapedPropIndent`"link`"\s*:") {
+ $lines[$j] = $lines[$j] -replace '"link"\s*:\s*"[^"]*"', "`"link`": `"$newLink`""
+ $linkUpdated = $true
+ break
}
- foreach ($feature in $itemDetails.feature) {
- $FeaturesDocs += "- $($feature)" + "`r`n"
+ if ($lines[$j] -match '^\s*\}') {
+ $null = $lines[$j] -match '^(\s*)'
+ if ($matches[1].Length -le $propIndentLen) {
+ $closeBraceIdx = $j
+ break
+ }
}
}
- # Clear the variable before continuing, will cause problems otherwise
- $InvokeScript = ""
- if ($itemDetails.InvokeScript) {
- $InvokeScriptContent = $itemDetails.InvokeScript | Out-String
- $InvokeScript = Process-MultilineStrings @"
- \\## Invoke Script
- \\
- \\``````powershell
- \\$InvokeScriptContent
- \\``````
-"@
- }
+ if (-not $linkUpdated -and $closeBraceIdx -ne -1) {
+ # Insert "link" before the closing brace
+ $prevPropIdx = $closeBraceIdx - 1
+ while ($prevPropIdx -gt $startIdx -and $lines[$prevPropIdx].Trim() -eq '') { $prevPropIdx-- }
- # Clear the variable before continuing, will cause problems otherwise
- $UndoScript = ""
- if ($itemDetails.UndoScript) {
- $UndoScriptContent = $itemDetails.UndoScript | Out-String
- $UndoScript = Process-MultilineStrings @"
- \\## Undo Script
- \\
- \\``````powershell
- \\$UndoScriptContent
- \\``````
-"@
+ if ($lines[$prevPropIdx] -notmatch ',\s*$') {
+ $lines[$prevPropIdx] = $lines[$prevPropIdx].TrimEnd() + ','
+ }
+ $lines.Insert($closeBraceIdx, "$propIndent`"link`": `"$newLink`"")
}
+ }
- # Clear the variable before continuing, will cause problems otherwise
- $ToggleScript = ""
- if ($itemDetails.ToggleScript) {
- $ToggleScriptContent = $itemDetails.ToggleScript | Out-String
- $ToggleScript = Process-MultilineStrings @"
- \\## Toggle Script
- \\
- \\``````powershell
- \\$ToggleScriptContent
- \\``````
-"@
- }
+ Set-Content -Path $JsonFilePath -Value $lines -Encoding utf8
+}
- # Clear the variable before continuing, will cause problems otherwise
- $ButtonScript = ""
- if ($itemDetails.ButtonScript) {
- $ButtonScriptContent = $itemDetails.ButtonScript | Out-String
- $ButtonScript = Process-MultilineStrings @"
- \\## Button Script
- \\
- \\``````powershell
- \\$ButtonScriptContent
- \\``````
-"@
- }
+# ==============================================================================
+# Main
+# ==============================================================================
+
+$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
+$repoRoot = Resolve-Path "$scriptDir/.."
+
+$tweaksJsonPath = "$repoRoot/config/tweaks.json"
+$featuresJsonPath = "$repoRoot/config/feature.json"
+$tweaksOutputDir = "$repoRoot/docs/content/dev/tweaks"
+$featuresOutputDir = "$repoRoot/docs/content/dev/features"
+$publicFunctionsDir = "$repoRoot/functions/public"
+$privateFunctionsDir = "$repoRoot/functions/private"
+
+$itemnametocut = 'WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?'
+$baseUrl = "https://winutil.christitus.com"
+
+# Categories with generated docs
+$documentedCategories = @(
+ "Essential Tweaks",
+ "z__Advanced Tweaks - CAUTION",
+ "Customize Preferences",
+ "Performance Plans",
+ "Features",
+ "Fixes",
+ "Legacy Windows Panels",
+ "Powershell Profile Powershell 7+ Only",
+ "Remote Access"
+)
+
+# Categories where Button entries embed a PS function instead of raw JSON
+$functionEmbedCategories = @(
+ "Fixes",
+ "Powershell Profile Powershell 7+ Only",
+ "Remote Access"
+)
- # Clear the variable before continuing, will cause problems otherwise
- $FunctionDetails = ""
- $processedFunctions = New-Object 'System.Collections.Generic.HashSet[System.String]'
- $allScripts = @($itemDetails.InvokeScript, $itemDetails.UndoScript, $itemDetails.ToggleScript, $itemDetails.ButtonScript)
- foreach ($script in $allScripts) {
- if ($script) {
- $calledFunctions = Get-CalledFunctions -scriptContent $script -functionList $functions -processedFunctions ([ref]$processedFunctions)
- foreach ($functionName in $calledFunctions) {
- if ($functions.ContainsKey($functionName)) {
- $FunctionDetails += Process-MultilineStrings @"
- \\## Function: $functionName
- \\
- \\``````powershell
- \\$($functions[$functionName])
- \\``````
- \\
-"@
- }
- }
- }
- }
+Update-Progress "Loading JSON files" 10
+$tweaks = Get-Content -Path $tweaksJsonPath -Raw | ConvertFrom-Json
+$features = Get-Content -Path $featuresJsonPath -Raw | ConvertFrom-Json
- $additionalFunctionToggle = Get-AdditionalFunctionsFromToggle -buttonName $fullItemName
- if ($additionalFunctionToggle) {
- $additionalFunctionNameToggle = "Invoke-$additionalFunctionToggle"
- if ($functions.ContainsKey($additionalFunctionNameToggle) -and -not $processedFunctions.Contains($additionalFunctionNameToggle)) {
- $FunctionDetails += Process-MultilineStrings @"
- \\## Function: $additionalFunctionNameToggle
- \\
- \\``````powershell
- \\$($functions[$additionalFunctionNameToggle])
- \\``````
- \\
-"@
- $processedFunctions.Add($additionalFunctionNameToggle)
- }
- }
+Update-Progress "Loading function files" 20
+$functionFiles = @{}
+Get-ChildItem -Path $publicFunctionsDir -Filter *.ps1 | ForEach-Object {
+ $functionFiles[$_.BaseName] = @{ Content = (Get-Content -Path $_.FullName -Raw).TrimEnd(); RelativePath = "functions/public/$($_.Name)" }
+}
+Get-ChildItem -Path $privateFunctionsDir -Filter *.ps1 | ForEach-Object {
+ $functionFiles[$_.BaseName] = @{ Content = (Get-Content -Path $_.FullName -Raw).TrimEnd(); RelativePath = "functions/private/$($_.Name)" }
+}
- $additionalFunctionButton = Get-AdditionalFunctionsFromButton -buttonName $fullItemName
- if ($additionalFunctionButton) {
- $additionalFunctionNameButton = "Invoke-$additionalFunctionButton"
- if ($functions.ContainsKey($additionalFunctionNameButton) -and -not $processedFunctions.Contains($additionalFunctionNameButton)) {
- $FunctionDetails += Process-MultilineStrings @"
- \\## Function: $additionalFunctionNameButton
- \\
- \\``````powershell
- \\$($functions[$additionalFunctionNameButton])
- \\``````
- \\
-"@
- $processedFunctions.Add($additionalFunctionNameButton)
- }
- }
+Update-Progress "Building button-to-function mapping" 30
+$buttonFunctionMap = Get-ButtonFunctionMapping -ButtonFilePath "$publicFunctionsDir/Invoke-WPFButton.ps1"
- # Clear the variable before continuing, will cause problems otherwise
- $registryDocs = ""
- if ($itemDetails.registry) {
- $registryDocs += Process-MultilineStrings @"
- \\## Registry Changes
- \\Applications and System Components store and retrieve configuration data to modify windows settings, so we can use the registry to change many settings in one place.
- \\
- \\
- \\You can find information about the registry on [Wikipedia](https://www.wikiwand.com/en/Windows_Registry) and [Microsoft's Website](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry).
- \\
- \\
-"@
- foreach ($regEntry in $itemDetails.registry) {
- $registryDocs += Process-MultilineStrings @"
- \\### Registry Key: $($regEntry.Name)
- \\
- \\**Type:** $($regEntry.Type)
- \\
- \\**Original Value:** $($regEntry.OriginalValue)
- \\
- \\**New Value:** $($regEntry.Value)
- \\
- \\
-"@
- }
- }
+Update-Progress "Updating documentation links in JSON" 40
+Add-LinkAttributeToJson -JsonFilePath $tweaksJsonPath -UrlPrefix "$baseUrl/dev/tweaks" -ItemNameToCut $itemnametocut
+Add-LinkAttributeToJson -JsonFilePath $featuresJsonPath -UrlPrefix "$baseUrl/dev/features" -ItemNameToCut $itemnametocut
- # Clear the variable before continuing, will cause problems otherwise
- $serviceDocs = ""
- if ($itemDetails.service) {
- $serviceDocs = Process-MultilineStrings @"
- \\## Service Changes
- \\
- \\Windows services are background processes for system functions or applications. Setting some to manual optimizes performance by starting them only when needed.
- \\
- \\You can find information about services on [Wikipedia](https://www.wikiwand.com/en/Windows_service) and [Microsoft's Website](https://learn.microsoft.com/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications).
- \\
- \\
-"@
- foreach ($service in $itemDetails.service) {
- $serviceDocs += Process-MultilineStrings @"
- \\### Service Name: $($service.Name)
- \\
- \\**Startup Type:** $($service.StartupType)
- \\
- \\**Original Type:** $($service.OriginalType)
- \\
- \\
-"@
- }
- }
+# Reload lines after link update so line numbers in docs are accurate
+$tweaksLines = Get-Content -Path $tweaksJsonPath
+$featuresLines = Get-Content -Path $featuresJsonPath
- # Clear the variable before continuing, will cause problems otherwise
- $scheduledTaskDocs = ""
- if ($itemDetails.ScheduledTask) {
- $scheduledTaskDocs = Process-MultilineStrings @"
- \\## Scheduled Task Changes
- \\
- \\Windows scheduled tasks are used to run scripts or programs at specific times or events. Disabling unnecessary tasks can improve system performance and reduce unwanted background activity.
- \\
- \\
- \\You can find information about scheduled tasks on [Wikipedia](https://www.wikiwand.com/en/Windows_Task_Scheduler) and [Microsoft's Website](https://learn.microsoft.com/en-us/windows/desktop/taskschd/about-the-task-scheduler).
- \\
- \\
-"@
- foreach ($task in $itemDetails.ScheduledTask) {
- $scheduledTaskDocs += Process-MultilineStrings @"
- \\### Task Name: $($task.Name)
- \\
- \\**State:** $($task.State)
- \\
- \\**Original State:** $($task.OriginalState)
- \\
- \\
-"@
- }
- }
+# ==============================================================================
+# Clean up old generated .md files (preserve _index.md)
+# ==============================================================================
- $jsonLink = "[View the JSON file](https://github.com/ChrisTitusTech/winutil/tree/main/$jsonFilePath)"
- $customContentStartTag = ""
- $customContentEndTag = ""
- $secondCustomContentStartTag = ""
- $secondCustomContentEndTag = ""
-
- if (Test-Path -Path "$filename") {
- $existingContent = Get-Content -Path "$filename" -Raw
- $customContentPattern = "(?s)$customContentStartTag(.*?)$customContentEndTag"
- $secondCustomContentPattern = "(?s)$secondCustomContentStartTag(.*?)$secondCustomContentEndTag"
- if ($existingContent -match $customContentPattern) {
- $customContent = $matches[1].Trim()
- }
- if ($existingContent -match $secondCustomContentPattern) {
- $secondCustomContent = $matches[1].Trim()
- }
- }
+Update-Progress "Cleaning up old generated docs" 45
+foreach ($dir in @($tweaksOutputDir, $featuresOutputDir)) {
+ Get-ChildItem -Path $dir -Recurse -Filter *.md | Where-Object {
+ $_.Name -ne "_index.md"
+ } | Remove-Item -Force
+}
- $fileContent = Process-MultilineStrings @"
- \\$header
- \\$lastUpdatedNotice
- \\
- \\$autoupdatenotice
- \\$( if ($itemDetails.Description) { $description } )
- \\
- \\$customContentStartTag
- \\$customContent
- \\$customContentEndTag
- \\
- \\$codeBlock
- \\
- \\$(
- if ($FeaturesDocs) { $FeaturesDocs + "`r`n" }
- if ($itemDetails.InvokeScript) { $InvokeScript + "`r`n" }
- if ($itemDetails.UndoScript) { $UndoScript + "`r`n" }
- if ($itemDetails.ToggleScript) { $ToggleScript + "`r`n" }
- if ($itemDetails.ButtonScript) { $ButtonScript + "`r`n" }
- if ($FunctionDetails) { $FunctionDetails + "`r`n" }
- if ($itemDetails.registry) { $registryDocs + "`r`n" }
- if ($itemDetails.service) { $serviceDocs + "`r`n" }
- if ($itemDetails.ScheduledTask) { $scheduledTaskDocs + "`r`n" }
- )
- \\$secondCustomContentStartTag
- \\$secondCustomContent
- \\$secondCustomContentEndTag
- \\
- \\
- \\$jsonLink
-"@
-
- Set-Content -Path "$filename" -Value "$fileContent" -Encoding utf8
-
- # TODO: For whatever reason, some headers have a space before them,
- # so as a temporary fix.. we'll remove these it so mkdocs can render properly
- (Get-Content -Raw -Path "$filename").Replace(' ##', '##') | Set-Content "$filename"
- $currentProgress += $progressIncrement
- $roundedProgress = [math]::Round($currentProgress)
- Update-Progress -StatusMessage "Generating content for documentation" -Percent $roundedProgress
- }
+# ==============================================================================
+# Generate Tweak Documentation
+# ==============================================================================
- return [PSCustomObject]@{
- TocEntries = $tocEntries
- ProcessedFiles = $processedFiles
- }
-}
+Update-Progress "Generating tweak documentation" 50
-function Generate-TypeSectionContent {
- param (
- [array]$entries
- )
+$tweakNames = $tweaks.PSObject.Properties.Name
+$totalTweaks = $tweakNames.Count
+$tweakCount = 0
- $totalEntries = $entries.Count
- $progressIncrement = 10 / $totalEntries
- $currentProgress = 90
+foreach ($itemName in $tweakNames) {
+ $item = $tweaks.$itemName
+ $tweakCount++
- $sectionContent = ""
- $categories = @{}
- foreach ($entry in $entries) {
- if (-Not $categories.ContainsKey($entry.Category)) {
- $categories[$entry.Category] = @()
- }
- $categories[$entry.Category] += $entry
+ if ($item.category -notin $documentedCategories) { continue }
- $currentProgress += $progressIncrement
- $roundedProgress = [math]::Round($currentProgress)
- Update-Progress -StatusMessage "Generating table of contents" -Percent $roundedProgress
- }
- foreach ($category in $categories.Keys) {
- $sectionContent += "### $category`r`n`r`n"
- foreach ($entry in $categories[$category]) {
- $sectionContent += "- [$($entry.Name)]($($entry.Path))`r`n"
- }
- }
- return $sectionContent
-}
+ $category = $item.category -replace '[^a-zA-Z0-9]', '-'
+ $displayName = $itemName -replace $itemnametocut, ''
+ $categoryDir = "$tweaksOutputDir/$category"
+ $filename = "$categoryDir/$displayName.md"
-function Add-LinkAttributeToJson {
- param (
- [string]$jsonFilePath,
- [string]$outputDir
- )
+ if (-Not (Test-Path -Path $categoryDir)) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
- $jsonText = Get-Content -Path $jsonFilePath -Raw
- $jsonData = $jsonText | ConvertFrom-Json
+ $title = $item.Content -replace '"', '\"'
+ $content = "---`r`ntitle: `"$title`"`r`ndescription: `"`"`r`n---`r`n`r`n"
- $totalItems = ($jsonData.PSObject.Properties | Measure-Object).Count
- $progressIncrement = 20 / $totalItems
- $currentProgress = 70
+ if ($item.Type -eq "Button") {
+ $funcName = $buttonFunctionMap[$itemName]
+ if ($funcName -and $functionFiles.ContainsKey($funcName)) {
+ $func = $functionFiles[$funcName]
+ $content += "``````powershell {filename=`"$($func.RelativePath)`",linenos=inline,linenostart=1}`r`n"
+ $content += $func.Content + "`r`n"
+ $content += "```````r`n"
+ }
+ } else {
+ $jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $tweaksLines
+ if ($jsonBlock) {
+ $content += "``````json {filename=`"config/tweaks.json`",linenos=inline,linenostart=$($jsonBlock.LineNumber)}`r`n"
+ $content += $jsonBlock.RawText + "`r`n"
+ $content += "```````r`n"
+ }
- foreach ($item in $jsonData.PSObject.Properties) {
- $itemName = $item.Name
- $itemDetails = $item.Value
- $category = $itemDetails.category -replace '[^a-zA-Z0-9]', '-'
- $displayName = $itemName -replace "$itemnametocut", ''
- $relativePath = "$outputDir/$category/$displayName" -replace '^docs/', ''
- $docLink = "https://christitustech.github.io/winutil/$relativePath"
- $jsonData.$itemName.link = $docLink
-
- $currentProgress += $progressIncrement
- $roundedProgress = [math]::Round($currentProgress)
- Update-Progress -StatusMessage "Adding documentation links to JSON" -Percent $roundedProgress
+ if ($item.registry) {
+ $content += "`r`n## Registry Changes`r`n`r`n"
+ $content += "Applications and System Components store and retrieve configuration data to modify Windows settings, so we can use the registry to change many settings in one place.`r`n`r`n"
+ $content += "You can find information about the registry on [Wikipedia](https://www.wikiwand.com/en/Windows_Registry) and [Microsoft's Website](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry).`r`n"
+ }
}
- $jsonText = ($jsonData | ConvertTo-Json -Depth 10).replace('\n',"`n").replace('\r', "`r")
- Set-Content -Path $jsonFilePath -Value ($jsonText) -Encoding utf8
-}
+ Set-Content -Path $filename -Value $content -Encoding utf8 -NoNewline
-Update-Progress "Loading JSON files" 10
-$tweaks = Get-Content -Path "../config/tweaks.json" | ConvertFrom-Json
-$features = Get-Content -Path "../config/feature.json" | ConvertFrom-Json
-
-Update-Progress "Getting last modified dates of the JSON files" 20
-$tweaksLastModified = (Get-Item "../config/tweaks.json").LastWriteTime.ToString("yyyy-MM-dd")
-$featuresLastModified = (Get-Item "../config/feature.json").LastWriteTime.ToString("yyyy-MM-dd")
-
-$tweaksOutputDir = "../docs/dev/tweaks"
-$featuresOutputDir = "../docs/dev/features"
-$privateFunctionsDir = "../functions/private"
-$publicFunctionsDir = "../functions/public"
-$functions = @{}
-$itemnametocut = "WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?"
-
-Update-Progress "Creating Directories" 30
-if (-Not (Test-Path -Path $tweaksOutputDir)) {
- New-Item -ItemType Directory -Path $tweaksOutputDir | Out-Null
-}
-if (-Not (Test-Path -Path $featuresOutputDir)) {
- New-Item -ItemType Directory -Path $featuresOutputDir | Out-Null
+ $percent = [Math]::Min(70, 50 + [int](($tweakCount / $totalTweaks) * 20))
+ Update-Progress "Generating tweak documentation ($tweakCount/$totalTweaks)" $percent
}
-Update-Progress "Loading existing Functions" 40
-Load-Functions -dir $privateFunctionsDir
-Load-Functions -dir $publicFunctionsDir
+# ==============================================================================
+# Generate Feature Documentation
+# ==============================================================================
-Update-Progress "Adding documentation links to JSON files" 50
+Update-Progress "Generating feature documentation" 70
-# Define the JSON file paths
-$jsonPaths = @("../config/feature.json", "../config/tweaks.json")
+$featureNames = $features.PSObject.Properties.Name
+$totalFeatures = $featureNames.Count
+$featureCount = 0
-# Loop through each JSON file path
-foreach ($jsonPath in $jsonPaths) {
- # Load the JSON content
- $json = Get-Content -Raw -Path $jsonPath | ConvertFrom-Json
+foreach ($itemName in $featureNames) {
+ $item = $features.$itemName
+ $featureCount++
- # Set the global root object to the current json object
- $global:rootObject = $json
+ if ($item.category -notin $documentedCategories) { continue }
+ if ($itemName -eq "WPFFeatureInstall") { continue }
- # Add the "link" attribute to the JSON
- Add-LinkAttribute -jsonObject $json
+ $category = $item.category -replace '[^a-zA-Z0-9]', '-'
+ $displayName = $itemName -replace $itemnametocut, ''
+ $categoryDir = "$featuresOutputDir/$category"
+ $filename = "$categoryDir/$displayName.md"
- # Convert back to JSON with the original formatting
- $jsonString = ($json | ConvertTo-Json -Depth 100).replace('\n',"`n").replace('\r', "`r")
+ if (-Not (Test-Path -Path $categoryDir)) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
- # Save the JSON back to the file
- Set-Content -Path $jsonPath -Value $jsonString
-}
+ $title = $item.Content -replace '"', '\"'
+ $content = "---`r`ntitle: `"$title`"`r`ndescription: `"`"`r`n---`r`n`r`n"
-Add-LinkAttributeToJson -jsonFilePath "../config/tweaks.json" -outputDir "dev/tweaks"
-Add-LinkAttributeToJson -jsonFilePath "../config/feature.json" -outputDir "dev/features"
-
-Update-Progress "Generating content for documentation" 60
-$tweakResult = Generate-MarkdownFiles -data $tweaks -outputDir $tweaksOutputDir -jsonFilePath "../config/tweaks.json" -lastModified $tweaksLastModified -type "tweak" -initialProgress 60
-$featureResult = Generate-MarkdownFiles -data $features -outputDir $featuresOutputDir -jsonFilePath "../config/feature.json" -lastModified $featuresLastModified -type "feature" -initialProgress 70
-
-Update-Progress "Generating table of contents" 80
-$allTocEntries = $tweakResult.TocEntries + $featureResult.TocEntries
-$tweakEntries = ($allTocEntries).where{ $_.Type -eq 'tweak' } | Sort-Object Category, Name
-$featureEntries = ($allTocEntries).where{ $_.Type -eq 'feature' } | Sort-Object Category, Name
-
-$indexContent += Process-MultilineStrings @"
- \\# Table of Contents
- \\
- \\
- \\## Tweaks
- \\
- \\
-"@
-$indexContent += $(Generate-TypeSectionContent $tweakEntries) + "`r`n"
-$indexContent += Process-MultilineStrings @"
- \\## Features
- \\
- \\
-"@
-$indexContent += $(Generate-TypeSectionContent $featureEntries) + "`r`n"
-Set-Content -Path "../docs/devdocs.md" -Value $indexContent -Encoding utf8
+ if ($item.category -in $functionEmbedCategories) {
+ $funcName = if ($item.function) { $item.function } else { $buttonFunctionMap[$itemName] }
+ if ($funcName -and $functionFiles.ContainsKey($funcName)) {
+ $func = $functionFiles[$funcName]
+ $content += "``````powershell {filename=`"$($func.RelativePath)`",linenos=inline,linenostart=1}`r`n"
+ $content += $func.Content + "`r`n"
+ $content += "```````r`n"
+ }
+ } else {
+ $jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $featuresLines
+ if ($jsonBlock) {
+ $content += "``````json {filename=`"config/feature.json`",linenos=inline,linenostart=$($jsonBlock.LineNumber)}`r`n"
+ $content += $jsonBlock.RawText + "`r`n"
+ $content += "```````r`n"
+ }
+ }
+
+ Set-Content -Path $filename -Value $content -Encoding utf8 -NoNewline
+
+ $percent = [Math]::Min(90, 70 + [int](($featureCount / $totalFeatures) * 20))
+ Update-Progress "Generating feature documentation ($featureCount/$totalFeatures)" $percent
+}
Update-Progress "Process Completed" 100
+Write-Host "Documentation generation complete."
diff --git a/windev.ps1 b/windev.ps1
index 6b627e08ed..6c74c59259 100644
--- a/windev.ps1
+++ b/windev.ps1
@@ -1,55 +1,13 @@
<#
.SYNOPSIS
This Script is used as a target for the https://christitus.com/windev alias.
- It queries the latest winget release (no matter if Pre-Release, Draft or Full Release) and invokes It
.DESCRIPTION
- This Script provides a simple way to always start the bleeding edge release even if it's not yet a full release.
- This function should be run with administrative privileges.
- Because this way of recursively invoking scripts via Invoke-Expression it might very well happen that AV Programs flag this because it's a common way of mulitstage exploits to run
+ This Script provides a simple way to start the bleeding edge release of winutil.
.EXAMPLE
irm https://christitus.com/windev | iex
OR
Run in Admin Powershell > ./windev.ps1
#>
-# Function to fetch the latest release tag from the GitHub API
-function Get-LatestRelease {
- try {
- $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/ChrisTitusTech/winutil/releases'
- $latestRelease = $releases | Where-Object {$_.prerelease -eq $true} | Select-Object -First 1
- return $latestRelease.tag_name
- } catch {
- Write-Host "Error fetching release data: $_" -ForegroundColor Red
- return $latestRelease.tag_name
- }
-}
-
-# Function to redirect to the latest pre-release version
-function RedirectToLatestPreRelease {
- $latestRelease = Get-LatestRelease
- if ($latestRelease) {
- $url = "https://github.com/ChrisTitusTech/winutil/releases/download/$latestRelease/winutil.ps1"
- } else {
- Write-Host 'No pre-release version found. This is most likely because the latest release is a full release and no newer pre-release exists.' -ForegroundColor Yellow
- Write-Host "Using latest Full Release"
- $url = "https://github.com/ChrisTitusTech/winutil/releases/latest/download/winutil.ps1"
- }
-
- $script = Invoke-RestMethod $url
- # Elevate Shell if necessary
- if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- Write-Output "Winutil needs to be run as Administrator. Attempting to relaunch."
-
- $powershellcmd = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
- $processCmd = if (Get-Command wt.exe -ErrorAction SilentlyContinue) { "wt.exe" } else { $powershellcmd }
-
- Start-Process $processCmd -ArgumentList "$powershellcmd -ExecutionPolicy Bypass -NoProfile -Command $(Invoke-Expression $script)" -Verb RunAs
- }
- else{
- Invoke-Expression $script
- }
-}
-
-# Call the redirect function
-
-RedirectToLatestPreRelease
+$latestTag = (Invoke-RestMethod "https://api.github.com/repos/ChrisTitusTech/winutil/tags")[0].name
+Invoke-RestMethod "https://github.com/ChrisTitusTech/winutil/releases/download/$latestTag/winutil.ps1" | Invoke-Expression
diff --git a/xaml/inputXML.xaml b/xaml/inputXML.xaml
index 39610760b0..8148adfda7 100644
--- a/xaml/inputXML.xaml
+++ b/xaml/inputXML.xaml
@@ -10,21 +10,36 @@
WindowStyle="None"
Width="Auto"
Height="Auto"
- MaxWidth="1380"
- MaxHeight="800"
- Title="Chris Titus Tech's Windows Utility">
+ MinWidth="800"
+ MinHeight="600"
+ Title="WinUtil">
+
+
+
+
+
+
+
+
+
+
+
+