Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/content/docs/guides/automation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ This is useful for:
- Reusing a known-good baseline after reinstalling Windows
- Standardizing deployments for labs, workstations, or personal setups

:::caution[Keep exported configurations current]
Exported configurations contain the WinUtil catalog keys that existed when the file was created. If a later WinUtil version removes or renames one of those keys, the import is rejected before any current selections are changed. PowerShell reports the stale entry as `Unknown selection key '<key>'`.

To recover, compare the reported key with the current files in the [WinUtil configuration catalog](https://github.com/ChrisTitusTech/winutil/tree/main/config). Remove or replace the stale key in your JSON file, or create and export a new configuration with the current WinUtil version, then run the import again. Re-export long-lived baselines after catalog changes so they remain compatible.
:::

:::note
Run the command in an elevated PowerShell session so WinUtil can apply system-level changes.
:::
58 changes: 56 additions & 2 deletions functions/private/Update-WinUtilSelections.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
function Update-WinUtilSelections ($flatJson) {
function Update-WinUtilSelections {
param(
[Parameter(Mandatory)]
[string[]]$flatJson,

[switch]$Replace
)

$nextSelections = @{
selectedApps = [System.Collections.Generic.List[string]]::new()
selectedTweaks = [System.Collections.Generic.List[string]]::new()
selectedToggles = [System.Collections.Generic.List[string]]::new()
selectedFeatures = [System.Collections.Generic.List[string]]::new()
selectedAppx = [System.Collections.Generic.List[string]]::new()
}

foreach ($cbkey in $flatJson) {

$listName = switch -Regex ($cbkey) {
Expand All @@ -9,6 +24,45 @@ function Update-WinUtilSelections ($flatJson) {
'^WPFAppx' { 'selectedAppx' }
}

$sync.$listName.Add($cbkey)
if (-not $listName) {
throw "Unsupported selection key '$cbkey'."
}

$isKnownSelection = switch ($listName) {
'selectedApps' {
$sync.configs.applicationsHashtable.ContainsKey($cbkey)
}
'selectedTweaks' {
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
}
'selectedToggles' {
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
}
'selectedFeatures' {
$null -ne $sync.configs.feature.PSObject.Properties[$cbkey]
}
'selectedAppx' {
$sync.configs.appxHashtable.ContainsKey($cbkey)
}
}

if (-not $isKnownSelection) {
throw "Unknown selection key '$cbkey'."
Comment thread
ChrisTitusTech marked this conversation as resolved.
}

$nextSelections[$listName].Add($cbkey)
}

if ($Replace) {
foreach ($listName in $nextSelections.Keys) {
$sync[$listName] = $nextSelections[$listName]
}
return
}

foreach ($listName in $nextSelections.Keys) {
foreach ($cbkey in $nextSelections[$listName]) {
$sync.$listName.Add($cbkey)
}
}
}
12 changes: 3 additions & 9 deletions functions/public/Invoke-WPFImpex.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,9 @@ function Invoke-WPFImpex {
return
}

# Clear all existing selections before importing so the import replaces
# the current state rather than merging with it
$sync.selectedAppx = [System.Collections.Generic.List[string]]::new()
$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
# Build and validate every imported selection before replacing the current
# state. This keeps a malformed config from leaving partial selections behind.
Update-WinUtilSelections -flatJson $flattenedJson -Replace

if ($sync.Form) {
Reset-WPFCheckBoxes -doToggles $true
Expand Down
120 changes: 118 additions & 2 deletions pester/ui-state.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,25 @@ namespace System.Windows.Controls

public class WrapPanel
{
public global::Windows.Visibility Visibility { get; set; }
public object Visibility { get; set; }
}

public class StackPanel
{
public System.Collections.ArrayList Children { get; } = new System.Collections.ArrayList();
public System.Collections.ArrayList Children { get; private set; }

public StackPanel()
{
Children = new System.Collections.ArrayList();
}
}
}
"@
}

. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFImpex.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
Expand Down Expand Up @@ -127,6 +133,16 @@ namespace System.Windows.Controls
Content = "Git"
}
}
appxHashtable = @{
WPFAppxExample = [pscustomobject]@{}
}
tweaks = [pscustomobject]@{
WPFTweaksTelemetry = [pscustomobject]@{}
WPFToggleDarkMode = [pscustomobject]@{}
}
feature = [pscustomobject]@{
WPFFeatureSandbox = [pscustomobject]@{}
}
}
WPFselectedAppsButton = [pscustomobject]@{
Content = ""
Expand Down Expand Up @@ -174,6 +190,106 @@ Describe "Update-WinUtilSelections" {
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
}

It "replaces selections only after every imported key is validated" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")

Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"WPFFeatureSandbox"
) -Replace

@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
}

It "preserves existing selections when an imported key is unsupported" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")

{
Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"NotAWinUtilKey"
) -Replace
} | Should -Throw "Unsupported selection key 'NotAWinUtilKey'."

@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
@($script:sync.selectedFeatures) | Should -Be @()
}

It "preserves existing selections when an imported key is not in the current catalog" {
$script:sync.selectedApps.Add("WPFInstallExisting")

{
Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"WPFInstallUnknown"
) -Replace
} | Should -Throw "Unknown selection key 'WPFInstallUnknown'."

@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @()
}
}

Describe "Invoke-WPFImpex import selection state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.Form = [pscustomobject]@{}

Mock Reset-WPFCheckBoxes { }
Mock Write-Error { }
}

AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}

It "replaces selections and resets the UI after a valid import" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
$configPath = Join-Path $TestDrive "valid-config.json"
@(
"WPFInstallGit",
"WPFFeatureSandbox"
) | ConvertTo-Json | Set-Content -LiteralPath $configPath

Invoke-WPFImpex -type "import" -Config $configPath

@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 1 -Exactly -ParameterFilter {
$doToggles -eq $true
}
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
}

It "preserves selections and does not reset the UI after an invalid import" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
$configPath = Join-Path $TestDrive "invalid-config.json"
@(
"WPFInstallGit",
"WPFInstallUnknown"
) | ConvertTo-Json | Set-Content -LiteralPath $configPath

Invoke-WPFImpex -type "import" -Config $configPath

@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
@($script:sync.selectedFeatures) | Should -Be @()
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 0 -Exactly
Should -Invoke -CommandName Write-Error -Times 1 -Exactly -ParameterFilter {
$Message -like "An error occurred while importing: *Unknown selection key 'WPFInstallUnknown'.*"
}
}
}

Describe "Invoke-WPFSelectedCheckboxesUpdate" {
Expand Down
Loading