Compare commits

...
Author SHA1 Message Date
Chris Titus 086aecf4b7 chore: Update generated dev docs and JSON links (#5014) 2026-08-19 17:24:59 -05:00
NikhilandChris Titus 436773df73 Fix legacy json import compatibility, UI sidebar refactor, and improved test coverage (#4911)
* Fix backward compatibility for old-style JSON config imports

* Refactor sidebar UI generation and add tests for Get-WinUtilVariables

* Fix appnavigation config test following UI generation refactor

* Restore global sync state in variables tests

* fix(impex): migrate supported legacy selections

Keep modern imports strict while allowing legacy backups to skip retired entries with clear logging and user feedback. Add fixtures covering partial and all-retired imports.

* test: strengthen UI and global state coverage

Exercise app category rendering through Initialize-WPFUI and restore global sync without leaking test state.

* fix(impex): ignore legacy metadata fields

Limit legacy selection migration to supported WPF key families so unrelated string metadata does not produce false retired-setting warnings.

* docs: clarify legacy config imports

Distinguish strict modern flat imports from partial legacy object migration, including single-setting export shape and historical groups.

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 17:13:14 -05:00
4b2fe55a7a fix(system): correct service, update repair, and legacy imports (#4966)
* fix(system): correct sc.exe argument syntax, reg deletion, and ipv6 dns guard

* fix(impex): exclude legacy Install metadata from flattened import

* fix(service): throw on non-zero LASTEXITCODE from sc.exe config

* refactor(dns): revert unnecessary ipv6 guard per review feedback

* test(impex): cover legacy config imports

---------

Co-authored-by: WinUtil Contributor <contributor@winutil.local>
Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 16:54:24 -05:00
Malin FossumandChris Titus b8b81edcdb Show preset JSON key in app, tweak, and feature tooltips (#4995)
* Add Get-WinUtilEntryToolTip helper for preset-key tooltips

* Show preset JSON key in app and tweak tooltips

* Match apps search against preset key for consistency with tweaks

* Limit preset-key tooltips to preset-representable controls

Comboboxes and package-manager radio buttons cannot appear in a preset
file. Update-WinUtilSelections routes a flat list of keys by the
WPFInstall/WPFTweaks/WPFToggle/WPFFeature/WPFAppx prefixes, so a preset
can carry neither a combobox selected value nor a radio group choice;
WingetRadioButton and ChocoRadioButton do not even carry a WPF prefix.
Advertising those control names as preset keys invited users to add keys
that fall through the switch and abort the whole import.

Revert the combobox label and radio button tooltips to the plain
description, and add tests that fail if the key is reattached to a
control the preset importer cannot accept.

* fix: address preset key review feedback

* docs: clarify preset key search scope

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 16:26:11 -05:00
Ivan LepekhaandChris Titus a7074963a6 fix(apps): warp prettify (#5013)
* fix(apps): WARP prettify

Updated Cloudflare WARP entry with new key and modified description and link.

* Fine, Chris, let's leave property name as full

* Rename 'cloudflare-warp' to 'CloudflareWARP'

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 16:11:04 -05:00
Ivan LepekhaandChris Titus 032ce8ead0 feat(github): revival of triage tools (#5007)
* feat(github): revival of triage tools

* fix(github): workflow doc fix

* fix(github): PR detection

* fix(gh): wontfix preserve bug label

Removed state_reason from issue update when adding wontfix label.

* fix(gh): doc cleaning

* one more thing

* fix(gh): prevent Gabi moment

That's the only thing I will agree with this piece of hardware

* Fix syntax error in triage-tools.yaml

* fix(github): restrict triage commands to issues

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 16:03:06 -05:00
792122e998 fix: preserve selections when config import is invalid (#4994)
* fix: validate imports before replacing selections

* test: cover atomic config imports

* docs: explain stale config imports

---------

Co-authored-by: Guilherme de Oliveira Rocha <contatoguilhermeoliveira@protonmail.com>
Co-authored-by: Chris Titus <contact@christitus.com>
2026-08-19 15:30:46 -05:00
2d0fd43bd3 Add EmulationStation and Tailscale entries to applications.json (#4973)
* Add EmulationStation and Tailscale entries to applications.json

* Update config/applications.json

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update config/applications.json

I'm so sorry I'm late I've had to take a break from this for a bit of health related stuff.

Co-authored-by: Ivan Lepekha <57459428+FluffyPunk@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ivan Lepekha <57459428+FluffyPunk@users.noreply.github.com>
2026-08-19 14:04:14 -05:00
Manreet Singh 219b2fe137 Added Cloudflare WARP to pro tools (#5011) 2026-08-19 14:03:22 -05:00
Ivan Lepekha 527eefb114 feat(apps): Added Battle.net (#4996)
* feat(apps): Added Battle.net

Just Blizzard. Well, we can hate them, but their launcher is unique

* You know, being smart is a hard work...

* titusTypo
2026-08-19 14:02:30 -05:00
MyDrift d1b8470af2 Link apps to their own site instead of a repo or store page (#5010)
* fix: link apps to their own site instead of a repo or store page

- the icon comes from the link's domain, so every GitHub hosted app showed
  the same octocat and the two store apps showed the Microsoft Store tile
- point 16 apps at the project's own site, and LibreWolf at librewolf.net
  rather than its old gitlab.io address
- left on GitHub where the site turned out to redirect back to the repo
  (fnm, MSEdgeRedirect, Deskflow) or has no icon to serve (gsudo,
  simplewall, ungoogled-chromium, DISMTools)

* fix: point Teams at teams.live.com so it shows the Teams logo

- the marketing page on microsoft.com serves the generic Microsoft favicon,
  which Edge already uses

* fix: keep GlazeWM and Lua on the pages that match what is installed

- glazewm.com is not the project's own site: the repo declares no homepage,
  glzr.io never references it, and it runs on unrelated hosting
- lua.org is the language, while the package is Lua for Windows

* fix: keep Teams on the Microsoft product page

- the package is Microsoft.Teams, the work product, while teams.live.com is
  the consumer edition
2026-08-19 14:00:57 -05:00
Malin Fossum 6066675f93 Add accessible name to font scaling slider (#4993)
FontScalingSlider had no AutomationProperties.Name, so screen readers
announce it as an unnamed slider with only a raw numeric value.
Completes the accessible-name coverage of the font scaling popup.
2026-08-19 14:00:12 -05:00
Malin Fossum b21d416161 Add accessible names to remaining top-bar controls (#4992)
ThemeButton, FontScalingButton, SearchBar, and SearchBarClearButton had
no AutomationProperties.Name, so screen readers (Narrator, NVDA)
announce them as raw glyph text ("N/A", Unicode E8D3), an unnamed edit
field, and a button called "X". Completes the top-bar naming pass
started for SettingsButton and the window controls.
2026-08-19 13:59:45 -05:00
Abdullah Mansour 87a0eb8637 Fix location service startup type (#4972)
* fix(tweaks): use canonical location service startup type

* test(tweaks): pin location service startup type
2026-08-19 13:50:34 -05:00
Omar 8500ed116f Fix OSCDIMG detection (#4983)
* fix: detect machine-wide OSCDIMG installations

* fix: repeat OSCDIMG discovery after installation
2026-08-19 13:49:38 -05:00
Omar d16ee0e9f1 chore: remove fullscreen optimizations tweak (#5000) 2026-08-19 13:46:34 -05:00
Chris Titus 5ba8686dd7 Update sponsors in README (#5003) 2026-08-18 10:40:06 -05:00
Chris Titus 3f1850f623 Update sponsors in README (#4988) 2026-08-16 10:35:29 -05:00
Chris Titus 0dbe39bc7d Update sponsors in README (#4985) 2026-08-15 10:35:25 -05:00
Chris Titus 91972b0853 Update support section with new product information 2026-08-14 08:44:10 -05:00
32 changed files with 1087 additions and 134 deletions
+212
View File
@@ -0,0 +1,212 @@
name: Repo Contributors' Issue Triage Tools
on:
issue_comment:
types: [created, edited]
jobs:
triage-tools:
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: none
contents: none
steps:
- name: Process issue command
uses: actions/github-script@v9
with:
script: |
const trustedUsers = [
7896101, // ChrisTitusTech
57459428, // FluffyPunk
125669256, // FallenGME
90123670, // mewclouds
121827219, // MyDrift-user
17331812, // Callum
101426328, // CodingWonders
70659536 // og-mrk
];
if (context.payload.issue.pull_request) {
console.log("Pull request comments are not supported. Exiting.");
return;
}
const comment = context.payload.comment;
const commentAuthor = comment.user;
if (!trustedUsers.includes(commentAuthor.id)) {
console.log(`Comment author ${commentAuthor.login} is not a trusted user. Exiting.`);
return;
}
const {owner, repo} = context.repo;
const issueNumber = context.issue.number;
const command = comment.body.trim().toLowerCase();
const triageMatch = command.match(/^\/triage$/);
const triageOffMatch = command.match(/^\/triageoff$/);
const notPlannedMatch = command.match(/^\/np(?:\s+(\w+))?$/);
const duplicateMatch = command.match(/^\/duplicate\s+#?(\d+)$/);
if (triageMatch) {
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.data.labels.some(label => label.name === "needs-triage")) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "This issue already has the 'needs-triage' label. Use /triageoff to remove it."
});
} else {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["needs-triage"]
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} requested maintainer triage for this issue.`
});
}
} else if (triageOffMatch) {
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.data.labels.some(label => label.name === "needs-triage")) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "needs-triage"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} removed the triage request from this issue.`
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "This issue is not currently marked for triage."
});
}
} else if (notPlannedMatch) {
const reason = notPlannedMatch[1];
if (reason === "wontfix") {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["wontfix"]
});
} else if (reason === "notrelated") {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["not-related"]
});
}
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
state_reason: "not_planned"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} closed this issue as not planned.`
});
} else if (duplicateMatch) {
const duplicateIssueNumber = Number(duplicateMatch[1]);
if (!Number.isSafeInteger(duplicateIssueNumber) || duplicateIssueNumber < 1) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "The duplicate target must be a positive issue number."
});
} else if (duplicateIssueNumber === issueNumber) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "An issue cannot be marked as a duplicate of itself."
});
} else {
let duplicateIssue;
try {
const response = await github.rest.issues.get({
owner,
repo,
issue_number: duplicateIssueNumber
});
duplicateIssue = response.data;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
if (duplicateIssue?.pull_request) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `#${duplicateIssueNumber} is a pull request, not an issue.`
});
} else if (duplicateIssue) {
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
state_reason: "duplicate",
duplicate_issue_id: duplicateIssue.id
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} closed this issue as a duplicate of #${duplicateIssueNumber}.`
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `Issue #${duplicateIssueNumber} does not exist.`
});
}
}
} else {
console.log("Comment does not contain a supported issue command. Exiting.");
return;
}
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id
});
+2 -2
View File
@@ -72,13 +72,13 @@ See https://github.com/ChrisTitusTech/winutil/blob/main/.github/CONTRIBUTING.md
## Support
- Leave a ⭐ to show support!
- EXE Wrapper for $10 @ https://www.cttstore.com/windows-toolbox
- Faster Dotnet Implementation for sale here: https://www.cttstore.com/windows-toolbox
## Sponsors
These are the sponsors that help keep this project alive with monthly contributions.
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/Abs313a"><img src="https:&#x2F;&#x2F;github.com&#x2F;Abs313a.png" width="60px" alt="User avatar: Abs" /></a><!-- sponsors -->
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/Abs313a"><img src="https:&#x2F;&#x2F;github.com&#x2F;Abs313a.png" width="60px" alt="User avatar: Abs" /></a><a href="https://github.com/kydoa"><img src="https:&#x2F;&#x2F;github.com&#x2F;kydoa.png" width="60px" alt="User avatar: Daniel Scheuermann" /></a><a href="https://github.com/hafiz-muhammad"><img src="https:&#x2F;&#x2F;github.com&#x2F;hafiz-muhammad.png" width="60px" alt="User avatar: Abdul Hafiz Muhammad" /></a><!-- sponsors -->
---
+62 -16
View File
@@ -98,6 +98,15 @@
"winget": "AutoHotkey.AutoHotkey",
"foss": true
},
"battlenet": {
"category": "Games",
"choco": "na",
"winget": "Blizzard.BattleNet",
"content": "Battle.net",
"description": "Battle.net is a launcher for games created and developed by Activision Blizzard",
"link": "https://battle.net",
"foss": false
},
"bitwarden": {
"category": "Utilities",
"choco": "bitwarden",
@@ -175,7 +184,7 @@
"choco": "na",
"content": "ChatGPT Desktop",
"description": "The official ChatGPT desktop app for Windows, distributed through the Microsoft Store.",
"link": "https://apps.microsoft.com/detail/9nt1r1c2hh7j",
"link": "https://openai.com/chatgpt/download/",
"winget": "msstore:9NT1R1C2HH7J",
"foss": false
},
@@ -202,7 +211,7 @@
"choco": "chromium",
"content": "Chromium",
"description": "Chromium is the open-source project that serves as the foundation for various web browsers, including Chrome.",
"link": "https://github.com/Hibbiki/chromium-win64",
"link": "https://www.chromium.org/",
"winget": "Hibbiki.Chromium",
"foss": true
},
@@ -328,7 +337,7 @@
"choco": "dorion",
"content": "Dorion",
"description": "Tiny alternative Discord client with a smaller footprint, snappier startup, themes, plugins and more!",
"link": "https://github.com/SpikeHD/Dorion",
"link": "https://spikehd.dev/projects/dorion/",
"winget": "SpikeHD.Dorion",
"foss": true
},
@@ -413,6 +422,16 @@
"winget": "Microsoft.Edge",
"foss": false
},
"es-de": {
"category": "Games",
"choco": "",
"content": "EmulationStation Desktop Edition",
"_comment": "This and emulationstation are two completely different things. ES-DE is your frontend for everything and has its own set of emulators. Emulationstation is a graphical frontend for RetroArch.",
"description": "EmulationStation Desktop Edition is a frontend for browsing and launching games from your multi-platform game collection.",
"link": "https://es-de.org/",
"winget": "ES-DE.EmulationStation-DE",
"foss": true
},
"enteauth": {
"category": "Utilities",
"choco": "ente-auth",
@@ -436,7 +455,7 @@
"choco": "files",
"content": "Files",
"description": "Alternative file explorer.",
"link": "https://github.com/files-community/Files",
"link": "https://files.community",
"winget": "FilesCommunity.Files",
"foss": true
},
@@ -607,7 +626,7 @@
"choco": "helium",
"content": "Helium",
"description": "Private, fast, and honest web browser.",
"link": "https://github.com/imputnet/helium/",
"link": "https://helium.computer",
"winget": "ImputNet.Helium",
"foss": true
},
@@ -616,7 +635,7 @@
"choco": "hugo-extended",
"content": "Hugo",
"description": "The world's fastest framework for building websites.",
"link": "https://github.com/gohugoio/hugo/",
"link": "https://gohugo.io",
"winget": "Hugo.Hugo.Extended",
"foss": true
},
@@ -733,7 +752,7 @@
"choco": "jellyfin-media-player",
"content": "Jellyfin Media Player",
"description": "Jellyfin Media Player is a client application for the Jellyfin media server, providing access to your media library.",
"link": "https://github.com/jellyfin/jellyfin-media-player",
"link": "https://jellyfin.org/",
"winget": "Jellyfin.JellyfinMediaPlayer",
"foss": true
},
@@ -823,7 +842,7 @@
"choco": "librewolf",
"content": "LibreWolf",
"description": "LibreWolf is a privacy-focused web browser based on Firefox, with additional privacy and security enhancements.",
"link": "https://librewolf-community.gitlab.io/",
"link": "https://librewolf.net/",
"winget": "LibreWolf.LibreWolf",
"foss": true
},
@@ -841,7 +860,7 @@
"choco": "mediainfo",
"content": "mpc-qt",
"description": "Media Player Classic Qute Theater",
"link": "https://github.com/mpc-qt/mpc-qt",
"link": "https://mpc-qt.github.io",
"winget": "mpc-qt.mpc-qt",
"foss": true
},
@@ -894,7 +913,7 @@
"choco": "mpc-hc-clsid2",
"content": "Media Player Classic - Home Cinema",
"description": "Media Player Classic - Home Cinema (MPC-HC) is a free and open-source video and audio player for Windows. MPC-HC is based on the original Guliverkli project and contains many additional features and bug fixes.",
"link": "https://github.com/clsid2/mpc-hc/",
"link": "https://mpc-hc.org/",
"winget": "clsid2.mpc-hc",
"foss": true
},
@@ -921,7 +940,7 @@
"choco": "mullvad-app",
"content": "Mullvad VPN",
"description": "This is the VPN client software for the Mullvad VPN service.",
"link": "https://github.com/mullvad/mullvadvpn-app",
"link": "https://mullvad.net/",
"winget": "MullvadVPN.MullvadVPN",
"foss": true
},
@@ -948,7 +967,7 @@
"choco": "nanazip",
"content": "NanaZip",
"description": "NanaZip is a fast and efficient file compression and decompression tool.",
"link": "https://github.com/M2Team/NanaZip",
"link": "https://nanazip.org",
"winget": "M2Team.NanaZip",
"foss": true
},
@@ -961,6 +980,15 @@
"winget": "Netbird.Netbird",
"foss": true
},
"tailscale": {
"category": "Utilities",
"choco": "tailscale",
"content": "Tailscale",
"description": "The Tailscale client allows you to connect all your devices using WireGuard®, without the hassle. Tailscale makes it as easy as installing an app and signing in.",
"link": "https://tailscale.com/",
"winget": "Tailscale.Tailscale",
"foss": false
},
"naps2": {
"category": "Document",
"choco": "naps2",
@@ -1374,6 +1402,15 @@
"winget": "qBittorrent.qBittorrent",
"foss": true
},
"qownnotes": {
"category": "Document",
"choco": "qownnotes",
"content": "QOwnNotes",
"description": "QOwnNotes is a free open-source note taking app with Nextcloud/ownCloud integration.",
"link": "https://www.qownnotes.org/",
"winget": "pbek.QOwnNotes",
"foss": true
},
"qtox": {
"category": "Communications",
"choco": "qtox",
@@ -1559,7 +1596,7 @@
"choco": "sunshine",
"content": "Sunshine/GameStream Server",
"description": "Sunshine is a GameStream server that allows you to remotely play PC games on Android devices, offering low-latency streaming.",
"link": "https://github.com/LizardByte/Sunshine",
"link": "https://app.lizardbyte.dev/Sunshine/",
"winget": "LizardByte.Sunshine",
"foss": true
},
@@ -1676,7 +1713,7 @@
"choco": "translucenttb",
"content": "TranslucentTB",
"description": "TranslucentTB is a tool that allows you to customize the transparency of the Windows Taskbar.",
"link": "https://github.com/TranslucentTB/TranslucentTB",
"link": "https://translucenttb.github.io",
"winget": "CharlesMilette.TranslucentTB",
"foss": true
},
@@ -1757,7 +1794,7 @@
"choco": "na",
"content": "Vesktop",
"description": "A cross platform electron-based desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.",
"link": "https://github.com/Vencord/Vesktop",
"link": "https://vesktop.dev",
"winget": "Vencord.Vesktop",
"foss": true
},
@@ -1847,7 +1884,7 @@
"choco": "na",
"content": "WhatsApp Desktop",
"description": "WhatsApp Desktop is the official Windows desktop messaging app from Meta, distributed through the Microsoft Store.",
"link": "https://apps.microsoft.com/detail/9nksqgp7f2nh",
"link": "https://www.whatsapp.com/download",
"winget": "msstore:9NKSQGP7F2NH",
"foss": false
},
@@ -2039,5 +2076,14 @@
"content": "Lua",
"link": "https://github.com/rjpcomputing/luaforwindows",
"foss": true
},
"CloudflareWARP": {
"category": "Utilities",
"choco": "warp",
"winget": "Cloudflare.Warp",
"description": "WARP is a freemium VPN service provided by Cloudflare. Includes usage of Cloudflare's DNS",
"content": "Cloudflare WARP",
"link": "https://one.one.one.one",
"foss": false
}
}
+1 -17
View File
@@ -114,7 +114,7 @@
"service": [
{
"Name": "lfsvc",
"StartupType": "Disable",
"StartupType": "Disabled",
"OriginalType": "Manual"
}
],
@@ -1168,22 +1168,6 @@
],
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/disablebgapps"
},
"WPFTweaksDisableFSO": {
"Content": "Fullscreen Optimizations - Disable",
"Description": "Disables FSO in all applications. NOTE: This will disable Color Management in Exclusive Fullscreen.",
"category": "z__Advanced Tweaks - CAUTION",
"panel": "1",
"registry": [
{
"Path": "HKCU:\\System\\GameConfigStore",
"Name": "GameDVR_DXGIHonorFSEWindowsCompatible",
"Value": "1",
"Type": "DWord",
"OriginalValue": "0"
}
],
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/disablefso"
},
"WPFTweaksDisableExplorerAutoDiscovery": {
"Content": "File Explorer Automatic Folder Discovery - Disable",
"Description": "Windows Explorer automatically tries to guess the type of the folder based on its contents, slowing down the browsing experience. WARNING! Will disable File Explorer grouping.",
+1
View File
@@ -56,6 +56,7 @@ export default defineConfig({
label: 'Code Reference',
items: [
{ label: 'Architecture & Design', slug: 'code-reference/architecture' },
{ label: 'Issue Triage Commands', slug: 'code-reference/issue-triage' },
{ label: 'Tweaks Reference', items: [{ autogenerate: { directory: 'code-reference/tweaks' } }] },
{ label: 'Features Reference', items: [{ autogenerate: { directory: 'code-reference/features' } }] },
],
@@ -125,9 +125,9 @@ function Invoke-WPFFixesUpdate {
if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate") {
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Removing WSUS client settings..." -PercentComplete 60
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -PercentComplete 0
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "AccountDomainSid", "/f" -RedirectStandardError "NUL"
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "PingID", "/f" -RedirectStandardError "NUL"
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "SusClientId", "/f" -RedirectStandardError "NUL"
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "AccountDomainSid" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "PingID" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "SusClientId" -ErrorAction SilentlyContinue
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -Status "Completed" -PercentComplete 100
}
@@ -0,0 +1,26 @@
---
title: Issue Triage Commands
description: Commands trusted maintainers can use to label and close GitHub issues.
---
Trusted repository members whose numeric GitHub user IDs are listed in the issue triage workflow can run the commands below. These commands work only on issues. Comments on pull requests are ignored, and the workflow token has no pull-request write permission.
## Commands
- `/triage` adds the `needs-triage` label.
- `/triageoff` removes the `needs-triage` label when it is present.
- `/np` closes the issue as not planned.
- `/np wontfix` adds the `wontfix` label without replacing existing labels, then closes the issue as not planned.
- `/np notrelated` adds the `not-related` label without replacing existing labels, then closes the issue as not planned.
- `/np <reason>` closes the issue as not planned without adding a label for unrecognized reasons.
- `/duplicate <issue number>` closes the issue as a duplicate of another issue. The positive issue number may optionally start with `#`; pull request numbers are rejected.
## Command handling
Put exactly one command in the comment. Leading and trailing whitespace is allowed, but text before or after the command is not. Command matching is case-insensitive, and malformed issue numbers are rejected.
After a command is handled, the workflow deletes the command comment and leaves an audit comment describing the result. A missing duplicate target or a request to duplicate an issue into itself is reported without closing the issue. Unexpected GitHub API failures stop the workflow and leave the command comment available for retry.
## Access
Access is granted by adding the trusted member's immutable numeric GitHub user ID to `.github/workflows/triage-tools.yaml` through a pull request. Abuse can result in removal from the allowlist or other repository moderation action.
@@ -1,6 +1,6 @@
---
title: "Multiplane Overlay"
description: "Multiplane Overlay compose multiple image layers, which can sometimes cause issues with graphics cards."
description: "Multiplane Overlay composes multiple image layers, which can sometimes cause issues with graphics cards. Changes to this preference are applied immediately."
editUrl: false
---
@@ -9,28 +9,40 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
:::
```json title="config/tweaks.json"
"WPFToggleMultiplaneOverlay": {
"WPFMultiplaneOverlay": {
"Content": "Multiplane Overlay",
"Description": "Multiplane Overlay compose multiple image layers, which can sometimes cause issues with graphics cards.",
"Description": "Multiplane Overlay composes multiple image layers, which can sometimes cause issues with graphics cards. Changes to this preference are applied immediately.",
"category": "Customize Preferences",
"panel": "2",
"Type": "Toggle",
"Type": "Combobox",
"ComboItems": "Enabled|Disabled (Compatibility)|Fully Disabled",
"ComboDescriptions": {
"Enabled": "Uses Windows' default overlay behavior.",
"Disabled (Compatibility)": "Disables MPO using OverlayTestMode=5, the less aggressive compatibility method.",
"Fully Disabled": "Disables MPO using OverlayTestMode=5 and DisableOverlays=1, the more aggressive method."
},
"registry": [
{
"Path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\Dwm",
"Name": "OverlayTestMode",
"Value": "0",
"Type": "DWord",
"OriginalValue": "5",
"DefaultState": "true"
"DefaultValue": "0",
"Values": {
"Enabled": "<RemoveEntry>",
"Disabled (Compatibility)": "5",
"Fully Disabled": "5"
}
},
{
"Path": "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers",
"Name": "DisableOverlays",
"Value": "0",
"Type": "DWord",
"OriginalValue": "1",
"DefaultState": "true"
"DefaultValue": "0",
"Values": {
"Enabled": "<RemoveEntry>",
"Disabled (Compatibility)": "<RemoveEntry>",
"Fully Disabled": "1"
}
}
],
}
@@ -17,7 +17,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"service": [
{
"Name": "lfsvc",
"StartupType": "Disable",
"StartupType": "Disabled",
"OriginalType": "Manual"
}
],
@@ -1,33 +0,0 @@
---
title: "Fullscreen Optimizations - Disable"
description: "Disables FSO in all applications. NOTE: This will disable Color Management in Exclusive Fullscreen."
editUrl: false
---
:::note
This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitusTech/winutil/blob/main/config/tweaks.json). Do not edit this page directly.
:::
```json title="config/tweaks.json"
"WPFTweaksDisableFSO": {
"Content": "Fullscreen Optimizations - Disable",
"Description": "Disables FSO in all applications. NOTE: This will disable Color Management in Exclusive Fullscreen.",
"category": "z__Advanced Tweaks - CAUTION",
"panel": "1",
"registry": [
{
"Path": "HKCU:\\System\\GameConfigStore",
"Name": "GameDVR_DXGIHonorFSEWindowsCompatible",
"Value": "1",
"Type": "DWord",
"OriginalValue": "0"
}
],
}
```
## 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://en.wikipedia.org/wiki/Windows_Registry) and [Microsoft's Website](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry).
@@ -14,6 +14,6 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
"category": "z__Advanced Tweaks - CAUTION",
"panel": "1",
"Type": "Combobox",
"ComboItems": "Default DHCP Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult",
"ComboItems": "Default DHCP Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult Mullvad Mullvad_Ads_Trackers Mullvad_Ads_Trackers_Malware Mullvad_Ads_Trackers_Malware_Social Mullvad_Ads_Trackers_Malware_Adult_Gambling Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social",
}
```
@@ -22,6 +22,8 @@ Example:
To view exactly what each preset does, see:
https://github.com/ChrisTitusTech/winutil/blob/main/config/preset.json
To find a key for a custom configuration, hover over a supported application, tweak, feature, or AppX entry in WinUtil. Its tooltip shows `Preset key: <key>`. Application keys are searchable on the Install tab, while tweak and AppX keys are searchable on their respective tabs. Feature keys are available from their tooltips because the Config/Features tab does not have search. Controls that cannot be applied from a preset, such as toggle switches, drop-downs, and package-manager choices, intentionally do not advertise a preset key.
To create your own config file:
1. Open WinUtil.
@@ -40,6 +42,14 @@ 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. Current exports use a flat JSON format: one string when a single setting is selected, or an array of strings when several settings are selected. If a later WinUtil version removes or renames any imported key, the entire import is rejected before current selections are changed. PowerShell reports the stale entry as `Unknown selection key '<key>'`.
Older WinUtil versions exported a JSON object with `Install` package metadata and grouped `WPFInstall`, `WPFTweaks`, `WPFToggle`, and `WPFFeature` selections. When importing one of these legacy files, WinUtil restores the keys that still exist and skips retired keys, recording them as a warning in the WinUtil log. If the file contains no supported selections, the import makes no changes.
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.
:::
@@ -5,9 +5,9 @@ function Find-AppsByNameOrDescription {
.DESCRIPTION
Search text and categories are independent filters that both have to pass. An entry is
shown when its name or description matches the search text, and when its category is in
the selected set. An empty search matches everything, and an empty category set matches
every category.
shown when its name, description, or application preset key matches the search text, and
when its category is in the selected set. An empty search matches everything, and an empty
category set matches every category.
While either filter is active the matching categories are expanded, since a collapsed
category would otherwise hide the very results that were asked for. With no filter at
@@ -115,7 +115,8 @@ function Find-AppsByNameOrDescription {
$categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category
$textMatch = -not $hasSearch -or
$appEntry.Content -like "*$escapedSearchString*" -or
$appEntry.Description -like "*$escapedSearchString*"
$appEntry.Description -like "*$escapedSearchString*" -or
$appTag -like "*$escapedSearchString*"
if ($categoryMatch -and $textMatch) {
$appControl.Visibility = [Windows.Visibility]::Visible
@@ -0,0 +1,25 @@
function Get-WinUtilEntryToolTip {
<#
.SYNOPSIS
Builds the tooltip string for an app/tweak/feature entry: its description plus its preset JSON key
.PARAMETER Description
The entry's description from the config JSON. May be null or empty.
.PARAMETER Key
The entry's JSON key as used in preset files (e.g. WPFInstallbrave, WPFTweaksTele).
#>
param(
[Parameter(Mandatory = $false)]
[string]$Description,
[Parameter(Mandatory = $true)]
[string]$Key
)
if ([string]::IsNullOrWhiteSpace($Description)) {
return "Preset key: $Key"
}
return "$Description`n`nPreset key: $Key"
}
@@ -19,7 +19,7 @@ function Initialize-InstallAppEntry {
$border = New-Object Windows.Controls.Border
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
$border.Tag = $appKey
$border.ToolTip = $app.description
$border.ToolTip = Get-WinUtilEntryToolTip -Description $app.description -Key $appKey
$border.Add_MouseLeftButtonUp({
# Resolve through $sync because the border's child is a layout Grid for FOSS entries
$childCheckbox = $sync.$($this.Tag)
@@ -14,7 +14,6 @@ function Initialize-WinUtilTabContent {
switch ($TabName) {
"Install" {
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
Initialize-WPFUI -targetGridName "appscategory"
Initialize-WPFUI -targetGridName "appspanel"
+30 -11
View File
@@ -529,6 +529,34 @@ function Invoke-WinUtilISOCleanAndReset {
$script.BeginInvoke()
}
function Get-WinUtilOSCDImgPath {
# Windows ADK installation
$oscdimg = Get-ChildItem "C:\Program Files (x86)\Windows Kits" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty FullName
if (-not $oscdimg) {
# Per-user winget installation
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName
}
if (-not $oscdimg) {
# Installation available through the current process PATH
$oscdimg = Get-Command oscdimg.exe -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty Source
}
if (-not $oscdimg) {
# WinGet links that may not yet be available through the current process PATH
$oscdimg = @(
"$env:LOCALAPPDATA\Microsoft\WinGet\Links\oscdimg.exe"
"$env:ProgramFiles\WinGet\Links\oscdimg.exe"
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}
return $oscdimg
}
function Invoke-WinUtilISOExport {
$contentsDir = $sync["Win11ISOContentsDir"]
@@ -551,14 +579,7 @@ function Invoke-WinUtilISOExport {
$outputISO = $dlg.FileName
# Locate oscdimg.exe (Windows ADK or winget per-user install)
$oscdimg = Get-ChildItem "C:\Program Files (x86)\Windows Kits" -Recurse -Filter "oscdimg.exe" |
Select-Object -First 1 -ExpandProperty FullName
if (-not $oscdimg) {
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName
}
$oscdimg = Get-WinUtilOSCDImgPath
if (-not $oscdimg) {
Write-WinUtilISOLog "oscdimg.exe not found. Attempting to install via winget..."
@@ -569,9 +590,7 @@ function Invoke-WinUtilISOExport {
$winget = Get-Command winget
$result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements
Write-WinUtilISOLog "winget output: $result"
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName
$oscdimg = Get-WinUtilOSCDImgPath
} catch {
Write-WinUtilISOLog "winget not available or install failed: $_"
}
+4 -1
View File
@@ -33,7 +33,10 @@ Function Set-WinUtilService {
# 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
sc.exe config $Name start= delayed-auto
if ($LASTEXITCODE -ne 0) {
throw "sc.exe config failed with exit code $LASTEXITCODE"
}
} else {
$service | Set-Service -StartupType $StartupType -ErrorAction Stop
}
+71 -2
View File
@@ -1,4 +1,21 @@
function Update-WinUtilSelections ($flatJson) {
function Update-WinUtilSelections {
param(
[Parameter(Mandatory)]
[string[]]$flatJson,
[switch]$Replace,
[switch]$SkipUnknown
)
$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) {
@@ -9,6 +26,58 @@ function Update-WinUtilSelections ($flatJson) {
'^WPFAppx' { 'selectedAppx' }
}
$sync.$listName.Add($cbkey)
if (-not $listName) {
if ($SkipUnknown) {
$cbkey
continue
}
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) {
if ($SkipUnknown) {
$cbkey
continue
}
throw "Unknown selection key '$cbkey'."
}
$nextSelections[$listName].Add($cbkey)
}
$validSelectionCount = ($nextSelections.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum
if ($SkipUnknown -and $validSelectionCount -eq 0) {
return
}
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)
}
}
}
+1 -4
View File
@@ -7,10 +7,7 @@ function Initialize-WPFUI {
switch ($TargetGridName) {
"appscategory"{
# TODO
# Switch UI generation of the sidebar to this function
# $sync.ItemsControl = Initialize-InstallAppArea -TargetElement $TargetGridName
# ...
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
# Create and configure a popup for displaying selected apps
$selectedAppsPopup = New-Object Windows.Controls.Primitives.Popup
+3 -3
View File
@@ -114,9 +114,9 @@ function Invoke-WPFFixesUpdate {
if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate") {
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Removing WSUS client settings..." -PercentComplete 60
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -PercentComplete 0
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "AccountDomainSid", "/f" -RedirectStandardError "NUL"
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "PingID", "/f" -RedirectStandardError "NUL"
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "SusClientId", "/f" -RedirectStandardError "NUL"
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "AccountDomainSid" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "PingID" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "SusClientId" -ErrorAction SilentlyContinue
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -Status "Completed" -PercentComplete 100
}
+55 -11
View File
@@ -73,9 +73,31 @@ function Invoke-WPFImpex {
Write-Error "Failed to load the JSON file from the specified path or URL: $_"
return
}
# 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
$isLegacyConfig = $jsonFile -is [System.Management.Automation.PSCustomObject] -and
$null -ne $jsonFile.PSObject.Properties["Install"] -and
$null -ne $jsonFile.PSObject.Properties["WPFInstall"]
if ($isLegacyConfig) {
Write-WinUtilLog -Component "Impex" -Message "Detected legacy WinUtil config structure; flattening import object."
# Legacy exports stored checkbox keys in WPFInstall and duplicated package
# source metadata in Install. Current package IDs come from the app catalog,
# so only the selection-key properties are restored.
$flattenedJson = @(
foreach ($property in $jsonFile.PSObject.Properties) {
if ($property.Name -notmatch '^WPF(?:Install|Tweaks|Toggle|Feature|Appx)') {
continue
}
foreach ($selection in @($property.Value)) {
if ($selection -is [string] -and -not [string]::IsNullOrWhiteSpace($selection)) {
$selection
}
}
}
)
} else {
# New style config: flat array of strings
$flattenedJson = $jsonFile
}
if (-not $flattenedJson) {
[System.Windows.MessageBox]::Show(
@@ -84,15 +106,37 @@ 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()
# Modern configs stay strict. Legacy configs can reference entries that no
# longer exist, so restore supported selections and report the retired keys.
if ($isLegacyConfig) {
$skippedSelections = @(Update-WinUtilSelections -flatJson $flattenedJson -Replace -SkipUnknown)
Update-WinUtilSelections -flatJson $flattenedJson
if ($skippedSelections.Count -gt 0) {
$skippedSummary = $skippedSelections -join ", "
Write-WinUtilLog -Component "Impex" -Level "WARN" -Message "Skipped unsupported legacy selections: $skippedSummary"
}
if ($skippedSelections.Count -eq @($flattenedJson).Count) {
if ($sync.Form) {
Show-WinUtilMessage -Message "This legacy configuration contains no settings supported by this version of WinUtil. No changes have been made." -Title "Unsupported Legacy Configuration" -Icon "Warning" | Out-Null
}
return
}
if ($skippedSelections.Count -gt 0) {
$skippedDisplay = @($skippedSelections | Select-Object -First 10) -join ", "
if ($skippedSelections.Count -gt 10) {
$skippedDisplay += "`n...and $($skippedSelections.Count - 10) more. See the WinUtil log for details."
}
if ($sync.Form) {
Show-WinUtilMessage -Message "Supported settings were imported. The following retired settings were skipped:`n`n$skippedDisplay" -Title "Legacy Configuration Partially Imported" -Icon "Warning" | Out-Null
}
}
} else {
# 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
+2 -2
View File
@@ -224,7 +224,7 @@ function Invoke-WPFUIElements {
$toggleButton = New-Object Windows.Controls.Primitives.ToggleButton
$toggleButton.Name = $entryInfo.Name
$toggleButton.Content = $entryInfo.Content[1]
$toggleButton.ToolTip = $entryInfo.Description
$toggleButton.ToolTip = Get-WinUtilEntryToolTip -Description $entryInfo.Description -Key $entryInfo.Name
$toggleButton.HorizontalAlignment = "Left"
$toggleButton.Style = $ToggleButtonStyle
[System.Windows.Automation.AutomationProperties]::SetName($toggleButton, $entryInfo.Content[0])
@@ -490,7 +490,7 @@ function Invoke-WPFUIElements {
$checkBox.Name = $entryInfo.Name
$checkBox.Content = $entryInfo.Content
$checkBox.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "FontSize")
$checkBox.ToolTip = $entryInfo.Description
$checkBox.ToolTip = Get-WinUtilEntryToolTip -Description $entryInfo.Description -Key $entryInfo.Name
$checkBox.SetResourceReference([Windows.Controls.Control]::MarginProperty, "CheckBoxMargin")
$checkBox.UseLayoutRounding = $true
[System.Windows.Automation.AutomationProperties]::SetName($checkBox, $entryInfo.Content)
+20 -1
View File
@@ -217,6 +217,16 @@ Describe "Tweaks config" {
throw ($invalidTweaks -join "`n")
}
}
It "keeps the location service disabled" -TestCases $testCase {
param([string]$Path)
$tweaks = Get-Content -Path $Path -Raw | ConvertFrom-Json
$locationServices = @($tweaks.WPFTweaksLocation.service | Where-Object Name -eq "lfsvc")
$locationServices | Should -HaveCount 1
$locationServices[0].StartupType | Should -Be "Disabled"
}
}
Describe "Preset config" {
@@ -254,8 +264,9 @@ Describe "App navigation config" {
It "is wired to an existing XAML target grid" {
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$tabInitializerScript = Get-Content -Path (Join-Path $script:repoRoot "functions/private/Initialize-WinUtilTabContent.ps1") -Raw
$uiInitializerScript = Get-Content -Path (Join-Path $script:repoRoot "functions/public/Initialize-WPFUI.ps1") -Raw
$targetGridMatch = [regex]::Match(
"$mainScript`n$tabInitializerScript",
"$mainScript`n$tabInitializerScript`n$uiInitializerScript",
'Invoke-WPFUIElements\s+-configVariable\s+\$sync\.configs\.appnavigation\s+-targetGridName\s+"([^"]+)"'
)
@@ -428,6 +439,7 @@ Describe "UI-rendered config entries" {
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$requiredFields = @("Content", "category", "panel", "link")
$allowedTypes = @("Button", "Combobox", "Toggle", "ToggleButton")
$allowedServiceStartupTypes = @("Automatic", "AutomaticDelayedStart", "Disabled", "Manual")
$supportedButtons = Get-WinUtilButtonSwitchNames
$invalidEntries = New-Object System.Collections.Generic.List[string]
@@ -498,6 +510,13 @@ Describe "UI-rendered config entries" {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName "$($entry.Name),service" -Entry $serviceEntry -RequiredFields @("Name", "StartupType", "OriginalType"))) {
$invalidEntries.Add($missingField)
}
foreach ($startupTypeField in @("StartupType", "OriginalType")) {
if ((Test-WinUtilHasNonEmptyProperty -Object $serviceEntry -Name $startupTypeField) -and
$allowedServiceStartupTypes -notcontains $serviceEntry.$startupTypeField) {
$invalidEntries.Add("$($entry.Name),service has unsupported $startupTypeField '$($serviceEntry.$startupTypeField)'")
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"Install": [
{
"winget": "Git.Git",
"choco": "git"
},
{
"winget": "Retired.App",
"choco": "retired-app"
}
],
"WPFTweaks": [
"WPFTweaksTelemetry"
],
"WPFFeature": [
"WPFFeatureSandbox"
],
"WPFInstall": [
"WPFInstallGit",
"WPFInstallRetired"
]
}
+28 -6
View File
@@ -8,12 +8,10 @@ BeforeAll {
function Invoke-WPFUIElements {
param($configVariable, [string]$targetGridName, [int]$columncount)
}
function Initialize-WPFUI {
param([string]$TargetGridName)
}
function Invoke-WinUtilISOCheckExistingWork { }
function Reset-WPFCheckBoxes { param([bool]$doToggles) }
. (Join-Path $script:repoRoot "functions\public\Initialize-WPFUI.ps1")
. (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilTabContent.ps1")
}
@@ -41,9 +39,7 @@ Describe "Initialize-WinUtilTabContent" {
Initialize-WinUtilTabContent -TabName "Install"
Initialize-WinUtilTabContent -TabName "Install"
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$targetGridName -eq "appscategory" -and $columncount -eq 1
}
Should -Invoke -CommandName Initialize-WPFUI -Times 1 -Exactly -ParameterFilter {
$TargetGridName -eq "appscategory"
}
@@ -105,6 +101,32 @@ Describe "Initialize-WinUtilTabContent" {
}
}
Describe "Initialize-WPFUI" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = @{
appnavigation = [pscustomobject]@{}
}
})
Mock Invoke-WPFUIElements { throw "App category rendered" }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "renders app navigation through the app category target" {
{ Initialize-WPFUI -TargetGridName "appscategory" } | Should -Throw "App category rendered"
Should -Invoke -CommandName Invoke-WPFUIElements -Times 1 -Exactly -ParameterFilter {
$configVariable -eq $script:sync.configs.appnavigation -and
$targetGridName -eq "appscategory" -and
$columncount -eq 1
}
}
}
Describe "Startup lazy tab wiring" {
It "builds only install tab content before first paint" {
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
+13
View File
@@ -360,6 +360,19 @@ Describe "Find-AppsByNameOrDescription" {
$editorItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
}
It "matches apps by preset key" {
$browserItem = New-WinUtilAppSearchItem -Tag "WPFInstallBrowser"
$mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia"
$category = New-WinUtilAppCategory -Label "- Browsers" -Items @($browserItem, $mediaItem)
New-WinUtilAppSearchContext -Categories @($category)
Find-AppsByNameOrDescription -SearchString "WPFInstallBrowser"
$browserItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
It "treats wildcard characters as literal app search text" {
$literalItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral"
$mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia"
+118
View File
@@ -0,0 +1,118 @@
#===========================================================================
# Tests - Entry ToolTip Helper
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilEntryToolTip.ps1")
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
$applications = Get-Content (Join-Path $script:repoRoot "config\applications.json") -Raw | ConvertFrom-Json
$applicationsHashtable = @{}
foreach ($property in $applications.PSObject.Properties) {
$applicationsHashtable["WPFInstall$($property.Name)"] = $property.Value
}
$appx = Get-Content (Join-Path $script:repoRoot "config\appx.json") -Raw | ConvertFrom-Json
$appxHashtable = @{}
foreach ($property in $appx.PSObject.Properties) {
$appxHashtable[$property.Name] = $property.Value
}
$script:selectionConfigs = @{
applicationsHashtable = $applicationsHashtable
tweaks = Get-Content (Join-Path $script:repoRoot "config\tweaks.json") -Raw | ConvertFrom-Json
feature = Get-Content (Join-Path $script:repoRoot "config\feature.json") -Raw | ConvertFrom-Json
appxHashtable = $appxHashtable
}
# Map each control type the renderer handles to whether its branch adds the preset key.
# Read from the source AST so re-adding the helper to an unsupported branch fails the test.
$script:rendererClauses = @{}
$rendererPath = Join-Path $script:repoRoot "functions\public\Invoke-WPFUIElements.ps1"
$rendererAst = [System.Management.Automation.Language.Parser]::ParseFile($rendererPath, [ref]$null, [ref]$null)
$typeSwitch = $rendererAst.FindAll({
$args[0] -is [System.Management.Automation.Language.SwitchStatementAst]
}, $true) | Where-Object { $_.Clauses.Item1.Extent.Text -contains '"Combobox"' }
foreach ($clause in $typeSwitch.Clauses) {
$clauseName = $clause.Item1.Extent.Text.Trim('"')
$script:rendererClauses[$clauseName] = [bool]($clause.Item2.Extent.Text -match 'Get-WinUtilEntryToolTip')
}
$script:rendererClauses["default"] = [bool]($typeSwitch.Default.Extent.Text -match 'Get-WinUtilEntryToolTip')
$appRendererPath = Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1"
$script:appRenderer = Get-Content $appRendererPath -Raw
# Every entry the renderer draws, with the branch that draws it
$script:renderedEntries = foreach ($configName in "appnavigation", "tweaks", "feature", "appx") {
$config = Get-Content (Join-Path $script:repoRoot "config\$configName.json") -Raw | ConvertFrom-Json
foreach ($property in $config.PSObject.Properties) {
$clauseName = "default"
if ($property.Value.Type -and $script:rendererClauses.ContainsKey($property.Value.Type)) {
$clauseName = $property.Value.Type
}
[pscustomobject]@{
Config = $configName
Key = $property.Name
Clause = $clauseName
}
}
}
}
Describe "Get-WinUtilEntryToolTip" {
It "appends the preset key after the description" {
Get-WinUtilEntryToolTip -Description "Fast private browser" -Key "WPFInstallBrave" |
Should -Be "Fast private browser`n`nPreset key: WPFInstallBrave"
}
It "returns only the key line when description is null" {
Get-WinUtilEntryToolTip -Description $null -Key "WPFTweaksTele" |
Should -Be "Preset key: WPFTweaksTele"
}
It "returns only the key line when description is whitespace" {
Get-WinUtilEntryToolTip -Description " " -Key "WPFTweaksTele" |
Should -Be "Preset key: WPFTweaksTele"
}
It "returns a plain string, not a UI object" {
(Get-WinUtilEntryToolTip -Description "x" -Key "WPFTweaksTele").GetType().Name |
Should -Be "String"
}
}
Describe "Preset key tooltips" {
It "leaves unsupported controls unlabelled" {
# Comboboxes and radio buttons are not representable in the flat preset format.
# Toggle keys can be imported, but preset execution does not apply their state.
$script:rendererClauses["Toggle"] | Should -BeFalse
$script:rendererClauses["Combobox"] | Should -BeFalse
$script:rendererClauses["RadioButton"] | Should -BeFalse
}
It "labels entries created by the application renderer" {
$script:appRenderer | Should -Match '\$border\.ToolTip\s*=\s*Get-WinUtilEntryToolTip\s+-Description\s+\$app\.description\s+-Key\s+\$appKey'
}
It "labels every entry with a key the preset importer accepts" {
$labelled = @($script:renderedEntries | Where-Object { $script:rendererClauses[$_.Clause] })
$labelled.Count | Should -BeGreaterThan 0
foreach ($entry in $labelled) {
$sync = @{
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()
configs = $script:selectionConfigs
}
{ Update-WinUtilSelections -flatJson @($entry.Key) } |
Should -Not -Throw -Because "$($entry.Config).json key '$($entry.Key)' is shown as a preset key"
$imported = @($sync.Values | ForEach-Object { $_ })
$imported | Should -Contain $entry.Key -Because "importing '$($entry.Key)' must restore that selection"
}
}
}
+68
View File
@@ -0,0 +1,68 @@
#===========================================================================
# Tests - GitHub Issue Triage Workflow
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:workflowPath = Join-Path $script:repoRoot ".github\workflows\triage-tools.yaml"
$script:guidePath = Join-Path $script:repoRoot "docs\src\content\docs\code-reference\issue-triage.mdx"
$script:workflow = Get-Content -Path $script:workflowPath -Raw
$script:guide = Get-Content -Path $script:guidePath -Raw
}
Describe "GitHub issue triage workflow" {
It "cannot run against pull requests" {
$script:workflow | Should -Match '(?m)^\s+if:\s+\$\{\{\s*!github\.event\.issue\.pull_request\s*\}\}\s*$'
$script:workflow | Should -Match 'if \(context\.payload\.issue\.pull_request\)'
$script:workflow | Should -Match '(?m)^\s+issues:\s+write\s*$'
$script:workflow | Should -Match '(?m)^\s+pull-requests:\s+none\s*$'
$script:workflow | Should -Match '(?m)^\s+contents:\s+none\s*$'
$script:workflow | Should -Not -Match '(?m)^\s+pull-requests:\s+write\s*$'
}
It "authorizes trusted users by immutable numeric ID" {
$script:workflow | Should -Match 'trustedUsers\.includes\(commentAuthor\.id\)'
$script:workflow | Should -Not -Match 'trustedUsers\.includes\(commentAuthor\.login\)'
}
It "matches one complete command at a time" {
$script:workflow.Contains('const command = comment.body.trim().toLowerCase();') | Should -BeTrue
$script:workflow.Contains('const triageMatch = command.match(/^\/triage$/);') | Should -BeTrue
$script:workflow.Contains('const triageOffMatch = command.match(/^\/triageoff$/);') | Should -BeTrue
$script:workflow.Contains('const notPlannedMatch = command.match(/^\/np(?:\s+(\w+))?$/);') | Should -BeTrue
$script:workflow.Contains('const duplicateMatch = command.match(/^\/duplicate\s+#?(\d+)$/);') | Should -BeTrue
$script:workflow.Contains('} else if (triageOffMatch) {') | Should -BeTrue
$script:workflow.Contains('} else if (notPlannedMatch) {') | Should -BeTrue
$script:workflow.Contains('} else if (duplicateMatch) {') | Should -BeTrue
}
It "preserves existing labels for not-related closures" {
$notRelatedBlock = [regex]::Match(
$script:workflow,
'(?s)reason === "notrelated".*?github\.rest\.issues\.addLabels\(\{.*?labels: \["not-related"\].*?github\.rest\.issues\.update'
)
$notRelatedBlock.Success | Should -BeTrue
$notRelatedBlock.Value | Should -Not -Match 'issues\.update\(\{.*?labels:'
}
It "validates duplicate targets before closing the issue" {
$script:workflow.Contains('const duplicateIssueNumber = Number(duplicateMatch[1]);') | Should -BeTrue
$script:workflow | Should -Match 'Number\.isSafeInteger\(duplicateIssueNumber\)'
$script:workflow | Should -Match 'duplicateIssueNumber === issueNumber'
$script:workflow | Should -Match 'duplicateIssue = response\.data'
$script:workflow | Should -Match 'duplicateIssue\?\.pull_request'
$script:workflow | Should -Match 'error\.status !== 404'
$script:workflow | Should -Match 'throw error'
$script:workflow | Should -Match 'duplicate_issue_id: duplicateIssue\.id'
}
It "keeps the maintainer guide in the documentation site" {
Test-Path -Path (Join-Path $script:repoRoot ".github\TRIAGE_TOOLS.md") | Should -BeFalse
$script:guide | Should -Match 'These commands work only on issues\.'
$script:guide | Should -Match '`not-related` label without replacing existing labels'
$astroConfig = Get-Content -Path (Join-Path $script:repoRoot "docs\astro.config.mjs") -Raw
$astroConfig | Should -Match "slug: 'code-reference/issue-triage'"
}
}
+213 -2
View File
@@ -48,12 +48,17 @@ 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();
}
}
}
"@
@@ -61,6 +66,7 @@ namespace System.Windows.Controls
. (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")
@@ -87,6 +93,9 @@ namespace System.Windows.Controls
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function Show-WinUtilMessage {
param($Message, $Title, $Button, $Icon)
}
function script:New-WinUtilFakeCheckBox {
param([bool]$IsChecked = $false)
@@ -127,6 +136,16 @@ namespace System.Windows.Controls
Content = "Git"
}
}
appxHashtable = @{
WPFAppxExample = [pscustomobject]@{}
}
tweaks = [pscustomobject]@{
WPFTweaksTelemetry = [pscustomobject]@{}
WPFToggleDarkMode = [pscustomobject]@{}
}
feature = [pscustomobject]@{
WPFFeatureSandbox = [pscustomobject]@{}
}
}
WPFselectedAppsButton = [pscustomobject]@{
Content = ""
@@ -174,6 +193,198 @@ 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 { }
Mock Write-WinUtilLog { }
Mock Show-WinUtilMessage { }
}
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 "imports supported legacy selections and reports retired entries" {
$configPath = Join-Path $script:repoRoot "pester\fixtures\legacy-config.json"
Invoke-WPFImpex -type "import" -Config $configPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksTelemetry")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 1 -Exactly -ParameterFilter {
$doToggles -eq $true
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Component -eq "Impex" -and
$Level -eq "WARN" -and
$Message -eq "Skipped unsupported legacy selections: WPFInstallRetired"
}
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Title -eq "Legacy Configuration Partially Imported" -and
$Message -like "*WPFInstallRetired*"
}
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
}
It "ignores unrelated string metadata in a legacy configuration" {
$configPath = Join-Path $TestDrive "legacy-config-with-metadata.json"
[pscustomobject]@{
Install = @([pscustomobject]@{ winget = "Git.Git"; choco = "git" })
WPFInstall = @("WPFInstallGit")
ExportVersion = "1.0"
} | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $configPath
Invoke-WPFImpex -type "import" -Config $configPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 1 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Component -eq "Impex" -and
$Message -eq "Detected legacy WinUtil config structure; flattening import object."
}
Should -Invoke -CommandName Show-WinUtilMessage -Times 0 -Exactly
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
}
It "preserves selections when every legacy entry is retired" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$configPath = Join-Path $TestDrive "retired-legacy-config.json"
[pscustomobject]@{
Install = @([pscustomobject]@{ winget = "Retired.App"; choco = "retired-app" })
WPFTweaks = @()
WPFFeature = @()
WPFInstall = @("WPFInstallRetired")
} | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath $configPath
Invoke-WPFImpex -type "import" -Config $configPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 0 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Message -eq "Skipped unsupported legacy selections: WPFInstallRetired"
}
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Title -eq "Unsupported Legacy Configuration"
}
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
}
It "imports legacy selection groups without treating Install metadata as a selection" {
$legacyConfigPath = Join-Path $TestDrive "legacy-config.json"
[ordered]@{
Install = @(
[pscustomobject]@{
winget = "Git.Git"
choco = "git"
}
)
WPFInstall = @("WPFInstallGit")
WPFTweaks = @("WPFTweaksTelemetry")
WPFToggle = @("WPFToggleDarkMode")
WPFFeature = @("WPFFeatureSandbox")
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $legacyConfigPath
Invoke-WPFImpex -type "import" -Config $legacyConfigPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksTelemetry")
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
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" {
+60
View File
@@ -0,0 +1,60 @@
#===========================================================================
# Tests - Get-WinUtilVariables
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:originalSyncVariable = Get-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
if ($script:originalSyncVariable) {
$script:originalSyncValue = $script:originalSyncVariable.Value
}
$global:sync = [Hashtable]::Synchronized(@{})
# Setup some test variables
$global:sync["WPFTestString"] = "I am a string"
$global:sync["WPFTestObj1"] = [PSCustomObject]@{ Name = "Test1" }
$global:sync["WPFTestObj2"] = [PSCustomObject]@{ Name = "Test2" }
$global:sync["WPFTestButton"] = [System.Version]::new("1.0.0.0")
$global:sync["OtherVar"] = "Not a WPF variable"
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilVariables.ps1")
}
AfterAll {
if ($script:originalSyncVariable) {
Set-Variable -Name sync -Value $script:originalSyncValue -Scope Global -Force
} else {
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
}
Describe "Get-WinUtilVariables" {
It "returns all WPF-prefixed keys when no type is provided" {
$result = Get-WinUtilVariables
$result.Count | Should -Be 4
$result | Should -Contain "WPFTestString"
$result | Should -Contain "WPFTestObj1"
$result | Should -Contain "WPFTestObj2"
$result | Should -Contain "WPFTestButton"
$result | Should -Not -Contain "OtherVar"
}
It "returns only WPF keys matching the specified exact type" {
$result = Get-WinUtilVariables -Type "String"
$result.Count | Should -Be 1
$result | Should -Contain "WPFTestString"
}
It "returns multiple objects matching PSCustomObject" {
$result = Get-WinUtilVariables -Type "PSCustomObject"
$result.Count | Should -Be 2
$result | Should -Contain "WPFTestObj1"
$result | Should -Contain "WPFTestObj2"
}
It "returns an empty list when no matching type is found" {
$result = Get-WinUtilVariables -Type "Int32"
$result | Should -BeNullOrEmpty
}
}
+7 -2
View File
@@ -1120,7 +1120,8 @@
Name="SearchBar"
Foreground="{DynamicResource MainForegroundColor}" Background="{DynamicResource MainBackgroundColor}"
Padding="3,3,30,0"
ToolTip="Press Ctrl-F and type app name to filter application list below. Press Esc to reset the filter">
ToolTip="Press Ctrl-F and type app name to filter application list below. Press Esc to reset the filter"
AutomationProperties.Name="Search">
</TextBox>
<TextBlock
Name="SearchBarIcon"
@@ -1136,6 +1137,7 @@
VerticalAlignment="Center" HorizontalAlignment="Right"
Name="SearchBarClearButton"
Style="{StaticResource SearchBarClearButtonStyle}"
AutomationProperties.Name="Clear Search"
Margin="0,0,20,0" Visibility="Collapsed">
</Button>
@@ -1153,6 +1155,7 @@
FontFamily="Segoe MDL2 Assets"
Content="N/A"
ToolTip="Change the WinUtil UI Theme"
AutomationProperties.Name="Theme"
/>
<Popup Name="ThemePopup"
IsOpen="False"
@@ -1191,6 +1194,7 @@
FontFamily="Segoe MDL2 Assets"
Content="&#xE8D3;"
ToolTip="Adjust Font Scaling for Accessibility"
AutomationProperties.Name="Font Scaling"
/>
<Popup Name="FontScalingPopup"
IsOpen="False"
@@ -1218,7 +1222,8 @@
TickPlacement="BottomRight"
IsSnapToTickEnabled="True"
Width="120"
VerticalAlignment="Center"/>
VerticalAlignment="Center"
AutomationProperties.Name="Font Scaling"/>
<TextBlock Text="Large"
FontSize="{DynamicResource ButtonFontSize}"
Foreground="{DynamicResource MainForegroundColor}"