Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9419b2803e | ||
|
|
92d5a08d49 | ||
|
|
a3a7c500d4 | ||
|
|
40666e1d31 | ||
|
|
4d4e219562 | ||
|
|
fc03af421b | ||
|
|
8939ecf4b4 | ||
|
|
735b59b82f | ||
|
|
d0d39d6478 | ||
|
|
6f1266eb86 | ||
|
|
48885d7c1e | ||
|
|
abcbc23144 | ||
|
|
311fde6f31 | ||
|
|
ad81e0a7af | ||
|
|
8e3998d9fd | ||
|
|
f65d2631a0 | ||
|
|
5f39d3b42d | ||
|
|
448543870a | ||
|
|
91a4f62068 | ||
|
|
2a73455872 | ||
|
|
52ebc931fd | ||
|
|
7aff652d4c | ||
|
|
be394d2ce0 | ||
|
|
b4181bd008 | ||
|
|
da7354bfc0 | ||
|
|
9de91c3999 | ||
|
|
c2d65b6f99 | ||
|
|
700725a878 | ||
|
|
f9139c75ff | ||
|
|
9ac45d6c31 | ||
|
|
2260df6365 | ||
|
|
fc60cbde09 | ||
|
|
dbe1c365f5 | ||
|
|
a0d3c719a6 | ||
|
|
283b9954f1 | ||
|
|
69ff04b632 | ||
|
|
7ee5be3d2f | ||
|
|
4a1db91e3f | ||
|
|
41e4f35897 | ||
|
|
d1a61790a1 | ||
|
|
87dd643176 | ||
|
|
8f7b12bc0d | ||
|
|
88071f2849 | ||
|
|
df858c4cd7 | ||
|
|
cc5e31460b | ||
|
|
d0f7689504 | ||
|
|
22e1dc9b09 | ||
|
|
c00f1eb7b3 | ||
|
|
f54b22da2c | ||
|
|
e15e2be1c4 | ||
|
|
34f7ce5a0e | ||
|
|
086aecf4b7 | ||
|
|
436773df73 | ||
|
|
4b2fe55a7a | ||
|
|
b8b81edcdb | ||
|
|
a7074963a6 | ||
|
|
032ce8ead0 | ||
|
|
792122e998 | ||
|
|
2d0fd43bd3 | ||
|
|
219b2fe137 | ||
|
|
527eefb114 | ||
|
|
d1b8470af2 | ||
|
|
6066675f93 | ||
|
|
b21d416161 | ||
|
|
87a0eb8637 | ||
|
|
8500ed116f | ||
|
|
d16ee0e9f1 | ||
|
|
5ba8686dd7 | ||
|
|
3f1850f623 | ||
|
|
0dbe39bc7d | ||
|
|
91972b0853 | ||
|
|
aee3e7a1f4 | ||
|
|
ea5de27b6c | ||
|
|
2752fe2e03 | ||
|
|
1458327638 | ||
|
|
ea7fcf9d2b | ||
|
|
0aa4ab3a40 | ||
|
|
afc3e1eec2 | ||
|
|
53fc260cc0 | ||
|
|
bc607b6c91 | ||
|
|
7f18b4fd60 | ||
|
|
32ab9f0ab0 | ||
|
|
9fdadd1c8f | ||
|
|
6de45a38b9 | ||
|
|
8860caf357 | ||
|
|
8fb078da4c | ||
|
|
95e3a54405 | ||
|
|
c817d3329a | ||
|
|
19f938e876 | ||
|
|
d64bb18717 | ||
|
|
5ad168e0c4 | ||
|
|
8d3adb599f | ||
|
|
f7c072341d | ||
|
|
32cc623959 | ||
|
|
b096aaa5e5 |
@@ -3,4 +3,7 @@
|
|||||||
|
|
||||||
# Seanh1917 (ANGRYxScotsmans) is the docs guy
|
# Seanh1917 (ANGRYxScotsmans) is the docs guy
|
||||||
docs/ @ChrisTitusTech @seanh1995
|
docs/ @ChrisTitusTech @seanh1995
|
||||||
tools/devdocs-generator.* @ChrisTitusTech @seanh1995
|
tools/devdocs-generator.* @ChrisTitusTech @seanh1995
|
||||||
|
|
||||||
|
# Title screen generation
|
||||||
|
tools/title-screen/ @ChrisTitusTech @mewclouds
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ graph TD
|
|||||||
* Run the following command to compile and run WinUtil:
|
* Run the following command to compile and run WinUtil:
|
||||||
* `.\Compile.ps1 -run`
|
* `.\Compile.ps1 -run`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
Open PowerShell as Administrator.
|
Open PowerShell as Administrator.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
name: "Bug report"
|
name: "Bug report"
|
||||||
description: "Report a bug to help us identify and fix issues in the project."
|
description: "Report a bug to help us identify and fix issues in the project."
|
||||||
|
title: "[Bug Report] - <your summary here>"
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
@@ -42,14 +43,16 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: issue_description
|
id: issue_description
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide a clear and concise description of the issue.
|
label: "Description of the issue"
|
||||||
|
description: Provide a clear and concise description of the issue.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: tweaks_applied
|
id: tweaks_applied
|
||||||
attributes:
|
attributes:
|
||||||
label: List the tweaks you applied before the issue occurred.
|
label: "List of Applied Tweaks"
|
||||||
|
description: List the tweaks you applied before the issue occurred.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
@@ -57,4 +60,5 @@ body:
|
|||||||
id: error_output
|
id: error_output
|
||||||
attributes:
|
attributes:
|
||||||
label: Paste the full error output (if available) or Screenshot or Video.
|
label: Paste the full error output (if available) or Screenshot or Video.
|
||||||
placeholder: "Include any relevant logs or error messages."
|
description: "If you're willing to share the whole log, please provide an external link to text-sharing service, i.e. Pastebin"
|
||||||
|
placeholder: "Include any relevant logs or error messages. Keep any sensitive info out of the presented log"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
name: "Feature request"
|
name: "Feature request"
|
||||||
description: "Suggest a new feature or improvement for the project."
|
description: "Suggest a new feature or improvement for the project."
|
||||||
|
title: "[Feature Request] - <your summary here>"
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
Read `AGENTS.md` in the repository root for operating instructions before doing anything else.
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
name: Generate WinUtil title screen
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: winutil-title-screen
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
WINUTIL_CAPTURE_WIDTH: "1920"
|
||||||
|
WINUTIL_CAPTURE_HEIGHT: "1080"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install uv and Python
|
||||||
|
uses: astral-sh/setup-uv@v10.2.0
|
||||||
|
with:
|
||||||
|
version: "0.12.0"
|
||||||
|
python-version: "3.13"
|
||||||
|
enable-cache: false
|
||||||
|
|
||||||
|
- name: Set display resolution
|
||||||
|
run: |
|
||||||
|
# Set-DisplayResolution is provided by Windows PowerShell's ServerCore
|
||||||
|
# module, so this step intentionally calls powershell.exe from pwsh.
|
||||||
|
& powershell.exe -NoLogo -NoProfile -Command @'
|
||||||
|
Set-DisplayResolution `
|
||||||
|
-Width $env:WINUTIL_CAPTURE_WIDTH `
|
||||||
|
-Height $env:WINUTIL_CAPTURE_HEIGHT `
|
||||||
|
-Force
|
||||||
|
'@
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to set the runner display resolution."
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-Type @"
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public static class ResolutionCheck {
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern int GetSystemMetrics(int index);
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
$actualWidth = [ResolutionCheck]::GetSystemMetrics(0)
|
||||||
|
$actualHeight = [ResolutionCheck]::GetSystemMetrics(1)
|
||||||
|
if (
|
||||||
|
$actualWidth -ne [int]$env:WINUTIL_CAPTURE_WIDTH -or
|
||||||
|
$actualHeight -ne [int]$env:WINUTIL_CAPTURE_HEIGHT
|
||||||
|
) {
|
||||||
|
throw (
|
||||||
|
"The hosted runner rejected the requested resolution. " +
|
||||||
|
"Requested: $env:WINUTIL_CAPTURE_WIDTH" +
|
||||||
|
"x$env:WINUTIL_CAPTURE_HEIGHT; " +
|
||||||
|
"actual: ${actualWidth}x${actualHeight}."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Report display environment
|
||||||
|
run: |
|
||||||
|
Add-Type @"
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public static class DisplayInfo {
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern int GetSystemMetrics(int index);
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
$width = [DisplayInfo]::GetSystemMetrics(0)
|
||||||
|
$height = [DisplayInfo]::GetSystemMetrics(1)
|
||||||
|
$sessionId = [Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||||
|
|
||||||
|
Write-Host "Resolution: ${width}x${height}"
|
||||||
|
Write-Host "Session ID: $sessionId"
|
||||||
|
Write-Host "User: $env:USERNAME"
|
||||||
|
|
||||||
|
- name: Compile WinUtil
|
||||||
|
run: |
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
./Compile.ps1
|
||||||
|
|
||||||
|
- name: Launch WinUtil
|
||||||
|
run: |
|
||||||
|
$scriptPath = Join-Path $env:GITHUB_WORKSPACE "winutil.ps1"
|
||||||
|
$command = "& '$scriptPath'"
|
||||||
|
$bytes = [Text.Encoding]::Unicode.GetBytes($command)
|
||||||
|
$encodedCommand = [Convert]::ToBase64String($bytes)
|
||||||
|
|
||||||
|
# Hide the console host while leaving the WPF window available.
|
||||||
|
$process = Start-Process powershell.exe -ArgumentList @(
|
||||||
|
"-NoLogo",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-EncodedCommand", $encodedCommand
|
||||||
|
) -WindowStyle Hidden -PassThru
|
||||||
|
|
||||||
|
"WINUTIL_HOST_PID=$($process.Id)" |
|
||||||
|
Out-File $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds(90)
|
||||||
|
do {
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
# A versioned title excludes the unversioned console host.
|
||||||
|
$window = Get-Process |
|
||||||
|
Where-Object { $_.MainWindowTitle -match '^WinUtil\s+\d' } |
|
||||||
|
Select-Object -First 1
|
||||||
|
} until ($window -or (Get-Date) -ge $deadline)
|
||||||
|
|
||||||
|
if (-not $window) {
|
||||||
|
throw "WinUtil did not expose a window within 90 seconds."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "WinUtil HWND: $($window.MainWindowHandle)"
|
||||||
|
Write-Host "WinUtil title: $($window.MainWindowTitle)"
|
||||||
|
|
||||||
|
# The exact HWND avoids desktop-enumeration ambiguity. Python validates
|
||||||
|
# the title and WPF class before using it.
|
||||||
|
"WINUTIL_HWND=$($window.MainWindowHandle)" |
|
||||||
|
Out-File $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
|
|
||||||
|
- name: Generate title screen
|
||||||
|
working-directory: tools/title-screen
|
||||||
|
run: |
|
||||||
|
& uv run --locked python automate_title_screen.py `
|
||||||
|
--output ..\..\docs\src\assets\branding\title-screen.png 2>&1 |
|
||||||
|
Tee-Object -FilePath "$env:RUNNER_TEMP\title-screen-capture.log"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Title-screen automation exited with code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create title-screen update pull request
|
||||||
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v8
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.AUTO_MERGE }}
|
||||||
|
add-paths: docs/src/assets/branding/title-screen.png
|
||||||
|
base: main
|
||||||
|
branch: title-screen-update
|
||||||
|
delete-branch: true
|
||||||
|
commit-message: "docs: update WinUtil title screen"
|
||||||
|
title: "docs: update WinUtil title screen"
|
||||||
|
body: |
|
||||||
|
Regenerates the WinUtil Light and Dark title-screen composite from the current main branch.
|
||||||
|
|
||||||
|
Source workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
labels: |
|
||||||
|
automated
|
||||||
|
documentation
|
||||||
|
skip-changelog
|
||||||
|
|
||||||
|
- name: Report pull request
|
||||||
|
if: steps.cpr.outputs.pull-request-url
|
||||||
|
env:
|
||||||
|
PR_OPERATION: ${{ steps.cpr.outputs.pull-request-operation }}
|
||||||
|
PR_URL: ${{ steps.cpr.outputs.pull-request-url }}
|
||||||
|
run: |
|
||||||
|
Write-Host "Pull request $env:PR_OPERATION`: $env:PR_URL"
|
||||||
|
|
||||||
|
- name: Inspect UI Automation on failure
|
||||||
|
if: failure()
|
||||||
|
continue-on-error: true
|
||||||
|
working-directory: tools/title-screen
|
||||||
|
run: |
|
||||||
|
uv run --locked python inspect_winutil.py `
|
||||||
|
"$env:RUNNER_TEMP\winutil-title-screen-inspect.txt"
|
||||||
|
|
||||||
|
- name: Upload failure diagnostics
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: winutil-title-screen-failure-${{ github.run_number }}
|
||||||
|
path: |
|
||||||
|
docs/src/assets/branding/title-screen.png
|
||||||
|
${{ runner.temp }}/title-screen-capture.log
|
||||||
|
${{ runner.temp }}/winutil-title-screen-inspect.txt
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Close WinUtil
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if ($env:WINUTIL_HOST_PID) {
|
||||||
|
Stop-Process `
|
||||||
|
-Id ([int]$env:WINUTIL_HOST_PID) `
|
||||||
|
-Force `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -10,11 +10,9 @@ testResults.xml
|
|||||||
# general software/os specific
|
# general software/os specific
|
||||||
desktop.ini
|
desktop.ini
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# hugo files (archived docs)
|
|
||||||
docs-old/public/
|
|
||||||
docs-old/.hugo_build.lock
|
|
||||||
docs-old/resources/
|
|
||||||
|
|||||||
@@ -4,35 +4,22 @@ Drop-in operating instructions for coding agents. Read this file before every ta
|
|||||||
|
|
||||||
**Working code only. Finish the job. Plausibility is not correctness.**
|
**Working code only. Finish the job. Plausibility is not correctness.**
|
||||||
|
|
||||||
This repository follows the AGENTS.md convention: these instructions are for Codex, Claude Code, Cursor, Windsurf, Copilot, Aider, Devin, Amp, and other coding agents that read `AGENTS.md`.
|
`SPEC.md` in the repository root is the project contract — read it for what WinUtil is and how it's architected. This file covers how to work on it.
|
||||||
|
|
||||||
## 0. Non-Negotiables
|
## 0. Non-Negotiables
|
||||||
|
|
||||||
These rules override everything else in this file when in conflict:
|
These rules override everything else in this file when in conflict:
|
||||||
|
|
||||||
1. **Do not edit `winutil.ps1` directly.** It is generated build output. Change source files and compile.
|
1. **Do not edit `winutil.ps1` directly.** It is generated build output (see SPEC.md's Build Model). Change source files and compile.
|
||||||
2. **Do not commit `winutil.ps1`.** It is ignored locally and generated by GitHub Actions for releases.
|
2. **Do not commit `winutil.ps1`.** It is ignored locally and generated by GitHub Actions for releases.
|
||||||
3. **Never fabricate.** Do not invent file paths, function names, command output, test results, commit hashes, or API behavior. Read the file or run the command.
|
3. **Never touch `docs/src/content/docs/code-reference/tweaks/` or `docs/src/content/docs/code-reference/features/`.** Both are auto-generated (see SPEC.md's Docs Site). Edit the source JSON (`config/tweaks.json`, `config/feature.json`) or the relevant PowerShell function file instead. Other hand-written pages under `code-reference/` (e.g. `architecture.mdx`) are not touched by the generator and may be edited directly.
|
||||||
4. **Disagree when the premise is wrong.** Say what is wrong before acting on it.
|
4. **Never fabricate.** Do not invent file paths, function names, command output, test results, commit hashes, or API behavior. Read the file or run the command.
|
||||||
5. **Stop when genuinely ambiguous.** If two interpretations would produce materially different diffs, ask before editing.
|
5. **Disagree when the premise is wrong.** Say what is wrong before acting on it.
|
||||||
6. **Touch only what the task requires.** No drive-by refactors, formatting sweeps, or unrelated cleanup.
|
6. **Stop when genuinely ambiguous.** If two interpretations would produce materially different diffs, ask before editing.
|
||||||
7. **Verify before saying done.** A plausible-looking diff is not proof.
|
7. **Touch only what the task requires.** No drive-by refactors, formatting sweeps, or unrelated cleanup.
|
||||||
|
8. **Verify before saying done.** A plausible-looking diff is not proof.
|
||||||
|
|
||||||
## 1. Project Context
|
## 1. Key Commands
|
||||||
|
|
||||||
WinUtil is a Windows PowerShell utility with a WPF interface. The repository is maintained as modular source, but the distributed artifact is one compiled PowerShell script.
|
|
||||||
|
|
||||||
### Stack
|
|
||||||
|
|
||||||
- Language: Windows PowerShell / PowerShell.
|
|
||||||
- UI: WPF via `xaml/inputXML.xaml`.
|
|
||||||
- Configuration: JSON files under `config/`.
|
|
||||||
- Tests: Pester tests under `pester/`.
|
|
||||||
- Lint: PowerShell Script Analyzer with settings in `lint/PSScriptAnalyser.ps1`.
|
|
||||||
- Docs: Hugo site under `docs/`.
|
|
||||||
- Release artifact: generated root `winutil.ps1`.
|
|
||||||
|
|
||||||
### Key Commands
|
|
||||||
|
|
||||||
- Compile:
|
- Compile:
|
||||||
```powershell
|
```powershell
|
||||||
@@ -42,48 +29,47 @@ WinUtil is a Windows PowerShell utility with a WPF interface. The repository is
|
|||||||
```powershell
|
```powershell
|
||||||
.\Compile.ps1 -Run
|
.\Compile.ps1 -Run
|
||||||
```
|
```
|
||||||
|
- Install the supported Pester version (one-time). `-SkipPublisherCheck` is required because Windows ships an inbox Pester 3.4.0 that is catalog-signed, and PowerShell Gallery's Pester 5.8.0 is Authenticode-signed — `Install-Module` refuses the upgrade without it. This does not skip download integrity (still HTTPS + NuGet package hash verification); `-Repository PSGallery` pins the trusted source explicitly rather than relying on whatever repositories happen to be registered:
|
||||||
|
```powershell
|
||||||
|
Install-Module -Name Pester -RequiredVersion 5.8.0 -Repository PSGallery -Scope CurrentUser -Force -SkipPublisherCheck
|
||||||
|
```
|
||||||
- Run tests:
|
- Run tests:
|
||||||
```powershell
|
```powershell
|
||||||
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
Import-Module Pester -RequiredVersion 5.8.0 -Force
|
||||||
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed
|
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
|
||||||
```
|
```
|
||||||
- Run Script Analyzer with project settings when available:
|
- Run Script Analyzer with project settings when available. If a locally compiled `winutil.ps1` exists, delete it first — `lint/PSScriptAnalyser.ps1` only excludes rules, not files, so `-Recurse` would also lint the generated script and produce noise against line numbers that don't map to any source file:
|
||||||
```powershell
|
```powershell
|
||||||
Invoke-ScriptAnalyzer -Path . -Settings .\lint\PSScriptAnalyser.ps1 -Recurse
|
Invoke-ScriptAnalyzer -Path . -Settings .\lint\PSScriptAnalyser.ps1 -Recurse
|
||||||
```
|
```
|
||||||
|
- Docs site dev server (run from `docs/`; see Section 2 for why this goes through Docker):
|
||||||
|
```powershell
|
||||||
|
docker compose up winutil-astro
|
||||||
|
```
|
||||||
|
- Docs site production build (run from `docs/`):
|
||||||
|
```powershell
|
||||||
|
docker compose run --rm winutil-astro npm run build
|
||||||
|
```
|
||||||
|
|
||||||
Prefer the narrowest useful verification while iterating. Use the full relevant check before finishing.
|
Prefer the narrowest useful verification while iterating. Use the full relevant check before finishing.
|
||||||
|
|
||||||
## 2. Source Of Truth
|
## 2. Dependency Installs, Builds, And Dev Servers
|
||||||
|
|
||||||
Make durable changes only in files consumed by `Compile.ps1` or in documentation/test files:
|
Given the current wave of npm/pnpm/yarn supply-chain worms (malicious postinstall/preinstall scripts, credential-stealing packages): **never run npm/pnpm/yarn/npx directly on the host, full stop.** The docs site (`docs/`) is the only npm-based project in this repo; always run its tooling inside Docker via `docs/Dockerfile` and `docs/docker-compose.yml` (service `winutil-astro`).
|
||||||
|
|
||||||
- `scripts/start.ps1` for startup/bootstrap code.
|
- Never run `npm install`, `npm run <script>`, `npx <pkg>`, `pnpm`, or `yarn` directly on the host shell in `docs/`. Use `docker compose run --rm winutil-astro <command>` / `docker compose up winutil-astro` instead (see Section 1 for the exact commands).
|
||||||
- `functions/public/*.ps1` for UI-facing and user-facing workflows.
|
- If a task needs a new docs dependency, add it to `docs/package.json` yourself, then rebuild the image and drop the `node_modules` volume so it repopulates from the new image (run from `docs/`): `docker compose build winutil-astro`, then `docker compose down -v`. Docker only seeds a named volume from the image the first time it's created, so a plain rebuild silently leaves the old `node_modules` in place. Don't install packages on the host, even temporarily, "just to check something."
|
||||||
- `functions/private/*.ps1` for internal helpers.
|
- If Docker isn't available on the host, propose the install command for the current OS and wait for confirmation before running it — don't fall back to running npm on the host instead. If the daemon just isn't running (Docker is installed but not started), tell the user rather than trying to start it yourself.
|
||||||
- `config/*.json` for applications, tweaks, features, DNS, presets, navigation, themes, and related declarative data.
|
- Treat any `postinstall`/`preinstall` lifecycle script in a new dependency as worth flagging to the user before installing — summarize what it does.
|
||||||
- `xaml/inputXML.xaml` for the WPF UI layout.
|
- Don't put real secrets anywhere under `docs/`. `docs/.dockerignore` only trims what `docker build` copies into the image — it does not affect the `docker compose` bind mount, which exposes the entire `docs/` directory (including any `.env` file) inside the container for every dev/build/preview command (see the next bullet). There is no "keep it out unless mounted" middle ground here.
|
||||||
- `tools/autounattend.xml` for the embedded unattended Windows setup template.
|
- The container mounts `docs/` as a volume, so file edits on the host are reflected inside the container immediately — no rebuild needed for normal code changes, only when `docs/package.json`/`docs/package-lock.json` change (see the rebuild-and-drop-volume steps above).
|
||||||
- `scripts/main.ps1` for the final entrypoint and GUI initialization logic appended during compile.
|
- This Docker requirement is specific to `docs/`. PowerShell tooling runs directly on the host per Section 1. The Python project under `tools/title-screen/` runs with uv as documented in its README.
|
||||||
- `pester/*.Tests.ps1` for automated checks.
|
|
||||||
- `docs/` for Hugo documentation.
|
|
||||||
|
|
||||||
If behavior changes require the compiled script to change, update these source files and run `.\Compile.ps1` only to verify generation.
|
## 3. Source Of Truth
|
||||||
|
|
||||||
## 3. Build Model
|
For changes that affect the compiled WinUtil script, make them only in the source files described in SPEC.md's Repository Layout — never in `winutil.ps1` itself. If behavior changes require the compiled script to change, update the source files and run `.\Compile.ps1` only to verify generation.
|
||||||
|
|
||||||
`Compile.ps1` combines the repository sources into `winutil.ps1` in this order:
|
This scoping applies to compiled-script behavior only. Repository metadata — `AGENTS.md`, `SPEC.md`, `CLAUDE.md`/`GEMINI.md`/`.github/copilot-instructions.md`, `.github/workflows/`, and the root `.gitignore` — is edited directly when a task requires it, per the other sections of this file.
|
||||||
|
|
||||||
1. Read `scripts/start.ps1` and replace `#{replaceme}` with the current `yy.MM.dd` build date.
|
|
||||||
2. Append every file under `functions/` recursively.
|
|
||||||
3. Convert each `config/*.json` file into embedded `$sync.configs` objects.
|
|
||||||
4. Special-case `config/applications.json` so keys receive the `WPFInstall` prefix in compiled config.
|
|
||||||
5. Embed `xaml/inputXML.xaml` into `$inputXML`.
|
|
||||||
6. Embed `tools/autounattend.xml` into `$WinUtilAutounattendXml`.
|
|
||||||
7. Append `scripts/main.ps1`.
|
|
||||||
8. Write the result to root `winutil.ps1`.
|
|
||||||
|
|
||||||
Because the final script is concatenated, do not rely on runtime module imports or source-relative dot-sourcing unless the compiled script will also contain the required code/data.
|
|
||||||
|
|
||||||
## 4. Before Editing
|
## 4. Before Editing
|
||||||
|
|
||||||
@@ -97,11 +83,10 @@ Because the final script is concatenated, do not rely on runtime module imports
|
|||||||
|
|
||||||
- Prefer the minimum code that solves the stated problem.
|
- Prefer the minimum code that solves the stated problem.
|
||||||
- Keep PowerShell functions in one function file when practical, with the file name matching the primary function name.
|
- Keep PowerShell functions in one function file when practical, with the file name matching the primary function name.
|
||||||
- Use approved PowerShell verb-noun names and follow the existing `WPF` / `WinUtil` naming conventions.
|
- Use approved PowerShell verb-noun names and follow the existing `WPF` / `WinUtil` naming conventions; keep UI event handler names aligned with XAML element names per SPEC.md's UI And Event Contract.
|
||||||
- Keep UI event handler names aligned with XAML element names. A button named `WPFExampleButton` is typically handled by `Invoke-WPFExampleButton`.
|
- Use `$sync` for shared state and UI references, consistent with SPEC.md's Runtime Model.
|
||||||
- Use `$sync` for shared state and UI references, consistent with the existing runspace model.
|
|
||||||
- Update WPF controls through the UI dispatcher when running work in a background runspace.
|
- Update WPF controls through the UI dispatcher when running work in a background runspace.
|
||||||
- Keep config-driven features in JSON when they fit the existing schema instead of hard-coding lists in PowerShell.
|
- Keep config-driven features in JSON when they fit the existing schema instead of hard-coding lists in PowerShell; follow SPEC.md's Configuration Contract for required fields and key-renaming rules.
|
||||||
- Preserve undo/original-state data for tweaks so users can reverse changes.
|
- Preserve undo/original-state data for tweaks so users can reverse changes.
|
||||||
- Do not add abstractions, configurability, hooks, or "future extensibility" unless the task needs them now.
|
- Do not add abstractions, configurability, hooks, or "future extensibility" unless the task needs them now.
|
||||||
- Clean up orphans created by your own changes, such as unused variables or functions made obsolete by the edit.
|
- Clean up orphans created by your own changes, such as unused variables or functions made obsolete by the edit.
|
||||||
@@ -109,7 +94,7 @@ Because the final script is concatenated, do not rely on runtime module imports
|
|||||||
|
|
||||||
## 6. Runtime And Safety Rules
|
## 6. Runtime And Safety Rules
|
||||||
|
|
||||||
- WinUtil performs system-level Windows changes. Treat registry, services, AppX removal, package manager, Windows Update, ISO, and unattended setup changes as high-risk.
|
- WinUtil performs system-level Windows changes; treat registry, services, AppX removal, package manager, Windows Update, ISO, and unattended setup changes as high-risk (see SPEC.md's Safety Requirements).
|
||||||
- Prefer existing helper functions for WinGet, Chocolatey, registry, services, progress, and UI updates.
|
- Prefer existing helper functions for WinGet, Chocolatey, registry, services, progress, and UI updates.
|
||||||
- Keep tweaks reversible where the schema supports it by including original values or original states.
|
- Keep tweaks reversible where the schema supports it by including original values or original states.
|
||||||
- Never modify a user's original ISO in-place; follow existing copy/mount/export patterns.
|
- Never modify a user's original ISO in-place; follow existing copy/mount/export patterns.
|
||||||
@@ -136,24 +121,27 @@ Define success in terms that can be checked, then check it.
|
|||||||
- Read command output. Do not report tests as passing unless they actually passed.
|
- Read command output. Do not report tests as passing unless they actually passed.
|
||||||
- If verification fails, fix the cause rather than weakening the test.
|
- If verification fails, fix the cause rather than weakening the test.
|
||||||
|
|
||||||
If a check cannot be run, say exactly why and what residual risk remains.
|
If a check cannot be run, say exactly why and what residual risk remains. See SPEC.md's Testing And CI for what GitHub Actions runs on every push.
|
||||||
|
|
||||||
## 9. Generated Files And Git Hygiene
|
## 9. Generated Files And Git Hygiene
|
||||||
|
|
||||||
- Treat local `winutil.ps1` changes as disposable compile output.
|
- Treat local `winutil.ps1` changes as disposable compile output.
|
||||||
- Never stage or commit `winutil.ps1`, `docs/public/`, `docs/resources/`, `binary/`, editor folders, or other ignored build artifacts.
|
- Never stage or commit `winutil.ps1`, `binary/`, or anything else ignored by the root `.gitignore` or `docs/.gitignore` — read those files rather than assuming. `docs/public/` is tracked source for static assets, not generated output.
|
||||||
|
- `docs/src/assets/branding/title-screen.png` is a tracked generated asset. Do not edit it manually. Update `tools/title-screen/` or run the title-screen workflow.
|
||||||
- Do not remove `.gitignore` rules that keep generated artifacts out of Git.
|
- Do not remove `.gitignore` rules that keep generated artifacts out of Git.
|
||||||
- Before finishing, check `git status --short` and separate your changes from pre-existing user changes.
|
- Before finishing, check `git status --short` and separate your changes from pre-existing user changes.
|
||||||
- Do not revert user changes unless explicitly asked.
|
- Do not revert user changes unless explicitly asked.
|
||||||
- Commit messages, when requested, should be descriptive: short subject under 72 characters, body explaining why when needed.
|
- Commit messages, when requested, should be descriptive: short subject under 72 characters, body explaining why when needed.
|
||||||
|
- When committing, split changes into small, logical commits rather than one large commit, so each commit's diff is reviewable as a single group of related changes.
|
||||||
|
|
||||||
## 10. Documentation Expectations
|
## 10. Documentation Expectations
|
||||||
|
|
||||||
- Update `docs/content/` when user-facing behavior changes.
|
- Update `docs/src/content/docs/guides/` when user-facing behavior changes.
|
||||||
- Update developer docs when architecture, build flow, config schema, or contribution workflow changes.
|
- Update `docs/src/content/docs/code-reference/architecture.mdx` and other hand-written developer docs when architecture, build flow, config schema, or contribution workflow changes — but never hand-edit the auto-generated `code-reference/tweaks/` or `code-reference/features/` subfolders (see Non-Negotiables).
|
||||||
|
- Keep sidebar entries in `docs/astro.config.mjs` in sync with page slugs (see SPEC.md's Docs Site).
|
||||||
- Keep README changes brief and high-level.
|
- Keep README changes brief and high-level.
|
||||||
- Put detailed user and developer documentation under `docs/`.
|
- Put detailed user and developer documentation under `docs/`.
|
||||||
- Keep `SPEC.md` aligned with build/runtime contract changes.
|
- Keep SPEC.md aligned with project/architecture changes, and this file aligned with process changes.
|
||||||
|
|
||||||
## 11. Communication Style
|
## 11. Communication Style
|
||||||
|
|
||||||
@@ -187,8 +175,16 @@ When the user corrects an agent approach, add or tighten one concrete rule here
|
|||||||
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
|
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
|
||||||
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
|
||||||
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
|
||||||
|
- Keep UI helpers such as `Invoke-WPFUIThread` and `Step-WinUtilJob` safe to call without a window; the `-Preset` and `-Config` paths run the workflows before the form is created and before PresentationCore is loaded. Ask `Test-WinUtilUIAlive` rather than writing the `$sync.Form` / dispatcher / `HasShutdownStarted` check out by hand.
|
||||||
|
- Put long operations on the job layer with `Start-WinUtilJob` and report from them with `Step-WinUtilJob`; a job body must not set the busy flag, print its own banner, or carry its own try/catch/finally around the interface. `Invoke-WPFRunspace` directly is for fire-and-forget work that is not a job.
|
||||||
|
- Drain interface work that nobody is waiting for through `Start-WinUtilBackgroundQueue`; do not hand-roll another dequeue-and-re-post pump.
|
||||||
|
- Values a posted scriptblock needs travel as the dispatcher's argument or through `-Parameters`, never captured from the caller: a plain block resolves them when the dispatcher gets to it, and `GetNewClosure` binds command lookup to a copied scope. For the same reason, prefer a compiled `[action]` over `Invoke-WPFUIThread -Async` on hot re-posting paths, which marshals its body as text and recompiles it per post.
|
||||||
|
- Diagnostic scaffolding does not ship. Measure with it, then delete it.
|
||||||
|
- Have each Pester file load the assemblies and dot-source the functions it needs; several passed only because an earlier file in alphabetical order happened to load them.
|
||||||
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
|
||||||
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
|
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
|
||||||
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
|
- For Win11 Creator driver injection, inject storage controllers (`SCSIAdapter` / `HDC`) into `boot.wim` index 2 with DISM, not `$WinpeDriver$`. Keep `install.wim` on its own per-package `/Add-Driver` retry loop. For each image, add each root package folder with its own `/Add-Driver /Recurse` so a single bad driver cannot fail the rest, and skip any folder whose ancestor is already in the set, since that ancestor's `/Recurse` covers it. Warn per failure and commit only when at least one package was added; when none were, warn and discard rather than throwing, so the run still produces an ISO. The discard in the cleanup block carries both orphaned mounts and that intentional zero-added case; keep it. Do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export. Use `-LiteralPath` for driver export paths, since `%TEMP%` can contain wildcard characters.
|
||||||
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
|
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
|
||||||
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
|
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
|
||||||
|
- Public pull-request diffs may be sent to configured external review services without a separate privacy approval; do not block the review loop on upload authorization for this public repository.
|
||||||
|
- Keep install-tab favicon loading overlapped with app-entry rendering; do not replace native WPF loading with a deferred second phase unless visible completion time is proven no slower than `main`.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ $sync = [Hashtable]::Synchronized(@{})
|
|||||||
$sync.configs = @{}
|
$sync.configs = @{}
|
||||||
|
|
||||||
$script = (Get-Content -Path scripts\start.ps1) -replace '#{replaceme}', (Get-Date -Format 'yy.MM.dd')
|
$script = (Get-Content -Path scripts\start.ps1) -replace '#{replaceme}', (Get-Date -Format 'yy.MM.dd')
|
||||||
|
$isLocalCompile = -not [string]::Equals($env:GITHUB_ACTIONS, "true", [StringComparison]::OrdinalIgnoreCase)
|
||||||
|
$script = $script -replace '#{islocalcompile}', $isLocalCompile.ToString().ToLowerInvariant()
|
||||||
|
|
||||||
$script += Get-ChildItem -Path functions -Recurse -File | ForEach-Object {
|
$script += Get-ChildItem -Path functions -Recurse -File | ForEach-Object {
|
||||||
Get-Content -Path $_.FullName -Raw
|
Get-Content -Path $_.FullName -Raw
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# GEMINI.md
|
||||||
|
|
||||||
|
Read `AGENTS.md` in the repository root for operating instructions before doing anything else.
|
||||||
@@ -13,7 +13,7 @@ A curated compilation of Windows system tasks streamline **installs**, debloat w
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
> **WinUtil must be run as Administrator** Because it performs system-wide changes.
|
> **WinUtil must be run as Administrator** because it performs system-wide changes.
|
||||||
|
|
||||||
Open PowerShell or Terminal as admin, then run:
|
Open PowerShell or Terminal as admin, then run:
|
||||||
|
|
||||||
@@ -72,13 +72,15 @@ See https://github.com/ChrisTitusTech/winutil/blob/main/.github/CONTRIBUTING.md
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
- Leave a ⭐ to show 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
|
## Sponsors
|
||||||
|
|
||||||
These are the sponsors that help keep this project alive with monthly contributions.
|
These are the sponsors that help keep this project alive with monthly contributions.
|
||||||
|
|
||||||
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https://github.com/ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https://github.com/dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https://github.com/mews-se.png" width="60px" alt="User avatar: Martin" /></a><a href="https://github.com/jdiegmueller"><img src="https://github.com/jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https://github.com/robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https://github.com/paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https://github.com/djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https://github.com/anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https://github.com/FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https://github.com/DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https://github.com/DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https://github.com/KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https://github.com/andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/seanh1995"><img src="https://github.com/seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/Abs313a"><img src="https://github.com/Abs313a.png" width="60px" alt="User avatar: Abs" /></a><!-- sponsors -->
|
<!-- sponsors --><a href="https://github.com/dwelfusius"><img src="https://github.com/dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https://github.com/mews-se.png" width="60px" alt="User avatar: Martin" /></a><a href="https://github.com/jdiegmueller"><img src="https://github.com/jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https://github.com/robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https://github.com/paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https://github.com/djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https://github.com/anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https://github.com/FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https://github.com/DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https://github.com/DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https://github.com/KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/seanh1995"><img src="https://github.com/seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/F-L-Perez"><img src="https://github.com/F-L-Perez.png" width="60px" alt="User avatar: Fra · ppe" /></a><a href="https://github.com/josencarnacao"><img src="https://github.com/josencarnacao.png" width="60px" alt="User avatar: José Encarnação" /></a><!-- sponsors -->
|
||||||
|
|
||||||
|
*<sub>Sponsors with a recurring subscription also get access to the .NET alternative.</sub>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
# SPEC.md
|
# SPEC.md
|
||||||
|
|
||||||
## Project Contract
|
Project contract for WinUtil — what the project is, how it's built, and how it runs. Written for anyone, human or AI, who needs to understand the project itself.
|
||||||
|
|
||||||
WinUtil is a Windows PowerShell utility with a WPF interface. The repository is maintained as modular source files, but the released artifact is a single generated `winutil.ps1` script.
|
`AGENTS.md` in the repository root points here for these facts, and separately covers how an agent should behave while working in this repo. This file does not change based on who's reading it.
|
||||||
|
|
||||||
The compiled `winutil.ps1` is not source code for editing or review. It is generated by `Compile.ps1` and produced during release automation. All durable changes must be made to the source files that feed the compiler.
|
## Project Context
|
||||||
|
|
||||||
|
WinUtil is a Windows PowerShell utility with a WPF interface. The repository is maintained as modular source, but the distributed artifact is one compiled PowerShell script.
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
|
||||||
|
- Language: Windows PowerShell / PowerShell.
|
||||||
|
- UI: WPF via `xaml/inputXML.xaml`.
|
||||||
|
- Configuration: JSON files under `config/`.
|
||||||
|
- Tests: Pester tests under `pester/`.
|
||||||
|
- Lint: PowerShell Script Analyzer with settings in `lint/PSScriptAnalyser.ps1`.
|
||||||
|
- Docs: Astro + Starlight site under `docs/`, built independently of `Compile.ps1` (its own `package.json`/`node_modules`).
|
||||||
|
- Release artifact: generated root `winutil.ps1`.
|
||||||
|
|
||||||
|
### Repository Layout
|
||||||
|
|
||||||
|
- `Compile.ps1`: build script that creates `winutil.ps1`.
|
||||||
|
- `scripts/start.ps1`: startup/bootstrap segment used at the beginning of the compiled script.
|
||||||
|
- `scripts/main.ps1`: main entrypoint appended at the end of the compiled script.
|
||||||
|
- `functions/public/`: public/UI-facing PowerShell functions.
|
||||||
|
- `functions/private/`: internal helper PowerShell functions.
|
||||||
|
- `config/`: JSON configuration consumed at compile time and embedded into `$sync.configs`.
|
||||||
|
- `xaml/inputXML.xaml`: WPF UI markup embedded into the compiled script.
|
||||||
|
- `tools/autounattend.xml`: unattended setup XML embedded for Windows ISO workflows.
|
||||||
|
- `pester/`: Pester tests for config and function checks.
|
||||||
|
- `lint/PSScriptAnalyser.ps1`: PowerShell Script Analyzer settings.
|
||||||
|
- `tools/title-screen/`: uv project that captures WinUtil's Light and Dark themes and generates the title-screen composite.
|
||||||
|
- `docs/`: Astro + Starlight documentation site, with its own `package.json` and build independent of `Compile.ps1`.
|
||||||
|
- `winutil.ps1`: ignored generated build artifact.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
@@ -21,85 +49,68 @@ The compiled `winutil.ps1` is not source code for editing or review. It is gener
|
|||||||
- The GUI is not a separate packaged desktop application in this repository's normal release path.
|
- The GUI is not a separate packaged desktop application in this repository's normal release path.
|
||||||
- Generated files should not be reviewed as source changes.
|
- Generated files should not be reviewed as source changes.
|
||||||
|
|
||||||
## Repository Layout
|
## Build Model
|
||||||
|
|
||||||
- `Compile.ps1`: build script that creates `winutil.ps1`.
|
`Compile.ps1` combines the repository sources into `winutil.ps1` in this order:
|
||||||
- `scripts/start.ps1`: startup/bootstrap segment used at the beginning of the compiled script.
|
|
||||||
- `scripts/main.ps1`: main entrypoint appended at the end of the compiled script.
|
|
||||||
- `functions/public/`: public/UI-facing PowerShell functions.
|
|
||||||
- `functions/private/`: internal helper PowerShell functions.
|
|
||||||
- `config/`: JSON configuration consumed at compile time and embedded into `$sync.configs`.
|
|
||||||
- `xaml/inputXML.xaml`: WPF UI markup embedded into the compiled script.
|
|
||||||
- `tools/autounattend.xml`: unattended setup XML embedded for Windows ISO workflows.
|
|
||||||
- `pester/`: Pester tests for config and function checks.
|
|
||||||
- `lint/PSScriptAnalyser.ps1`: PowerShell Script Analyzer settings.
|
|
||||||
- `docs/`: Hugo documentation site.
|
|
||||||
- `winutil.ps1`: ignored generated build artifact.
|
|
||||||
|
|
||||||
## Compile Specification
|
1. Read `scripts/start.ps1` and replace `#{replaceme}` with the current `yy.MM.dd` build date.
|
||||||
|
2. Append every file under `functions/` recursively.
|
||||||
|
3. Convert each `config/*.json` file into embedded `$sync.configs` objects.
|
||||||
|
4. Special-case `config/applications.json` so keys receive the `WPFInstall` prefix in compiled config.
|
||||||
|
5. Embed `xaml/inputXML.xaml` into `$inputXML`.
|
||||||
|
6. Embed `tools/autounattend.xml` into `$WinUtilAutounattendXml`.
|
||||||
|
7. Append `scripts/main.ps1`.
|
||||||
|
8. Write the result to root `winutil.ps1`.
|
||||||
|
|
||||||
`Compile.ps1` must produce a standalone root `winutil.ps1` by combining all required project files.
|
Because the final script is concatenated, code cannot rely on runtime module imports or source-relative dot-sourcing unless the compiled script will also contain the required code/data.
|
||||||
|
|
||||||
The compile flow is:
|
|
||||||
|
|
||||||
1. Initialize shared state with `$sync = [Hashtable]::Synchronized(@{})` and `$sync.configs = @{}`.
|
|
||||||
2. Read `scripts/start.ps1`.
|
|
||||||
3. Replace `#{replaceme}` in startup code with the current `yy.MM.dd` build date.
|
|
||||||
4. Append raw content from all files under `functions/` recursively.
|
|
||||||
5. For every file in `config/`, parse JSON and embed it into `$sync.configs.<basename>`.
|
|
||||||
6. Special-case `config/applications.json` so application keys are emitted with the `WPFInstall` prefix.
|
|
||||||
7. Embed `xaml/inputXML.xaml` as `$inputXML`.
|
|
||||||
8. Embed `tools/autounattend.xml` as `$WinUtilAutounattendXml`.
|
|
||||||
9. Append `scripts/main.ps1`.
|
|
||||||
10. Write the combined script to `winutil.ps1`.
|
|
||||||
11. If `-Run` is supplied, execute the generated script.
|
|
||||||
|
|
||||||
The generated script must have everything it needs from repository sources embedded or appended by this process.
|
|
||||||
|
|
||||||
## Runtime Model
|
## Runtime Model
|
||||||
|
|
||||||
- WinUtil runs in PowerShell on Windows and uses WPF for the UI.
|
- WinUtil runs in PowerShell on Windows and uses WPF for the UI.
|
||||||
- Shared mutable state is stored in `$sync`, including configs, UI element references, runspace state, selections, and progress.
|
- Shared mutable state is stored in `$sync`, including configs, UI element references, runspace state, selections, and progress.
|
||||||
- Long-running operations should use runspaces or existing async patterns so the UI remains responsive.
|
- Long-running operations use runspaces or existing async patterns so the UI remains responsive.
|
||||||
- UI updates from background work must be dispatched back to the WPF UI thread.
|
- UI updates from background work are dispatched back to the WPF UI thread.
|
||||||
- Declarative features such as apps, tweaks, presets, DNS providers, and navigation should stay in `config/*.json` unless code is required.
|
- Declarative features such as apps, tweaks, presets, DNS providers, and navigation stay in `config/*.json` unless code is required.
|
||||||
|
|
||||||
## UI And Event Contract
|
## UI And Event Contract
|
||||||
|
|
||||||
- UI layout lives in `xaml/inputXML.xaml`.
|
- UI layout lives in `xaml/inputXML.xaml`.
|
||||||
- Named WPF controls are discovered and stored in `$sync`.
|
- Named WPF controls are discovered and stored in `$sync`.
|
||||||
- Button/action wiring follows existing naming conventions, where an element named like `WPFThingButton` maps to a function named like `Invoke-WPFThingButton`.
|
- Button/action wiring follows a naming convention: an element named like `WPFThingButton` maps to a function named like `Invoke-WPFThingButton`.
|
||||||
- When adding controls, ensure the XAML name, config key, and PowerShell function names line up with the existing event system.
|
|
||||||
|
|
||||||
## Configuration Contract
|
## Configuration Contract
|
||||||
|
|
||||||
Config files must remain valid JSON and compile cleanly through `ConvertFrom-Json`.
|
- Config files must remain valid JSON and compile cleanly through `ConvertFrom-Json`.
|
||||||
|
- `config/dns.json` opts unfiltered providers into Fastest selection with `BenchmarkEligible: true`; missing or false values exclude a provider from the TCP latency benchmark.
|
||||||
`config/applications.json` defines installable applications. Each application entry should include the fields expected by tests and UI code, such as package manager IDs, category, display content, description, and link.
|
- `config/applications.json` defines installable applications; each entry includes the fields expected by tests and UI code, such as package manager IDs, category, display content, description, and link.
|
||||||
|
- `config/tweaks.json` defines Windows tweaks; registry and service changes include original values or original states when applicable so undo workflows can restore user systems.
|
||||||
`config/tweaks.json` defines Windows tweaks. Registry and service changes should include original values or original states when applicable so undo workflows can restore user systems.
|
- Preset and navigation files reference valid config keys. Renaming a config key requires updating all presets, UI references, docs, and code paths together.
|
||||||
|
|
||||||
Preset and navigation files should reference valid config keys. Avoid renaming config keys unless all presets, UI references, docs, and code paths are updated together.
|
|
||||||
|
|
||||||
## Safety Requirements
|
## Safety Requirements
|
||||||
|
|
||||||
- Registry, service, package manager, Windows Update, AppX removal, and ISO operations can affect the host system. Changes must be explicit, reversible where practical, and consistent with existing logging and confirmation patterns.
|
- Registry, service, package manager, Windows Update, AppX removal, and ISO operations affect the host system and are treated as high-risk.
|
||||||
- Tweak changes should include undo metadata when the schema supports it.
|
- Tweak changes include undo metadata when the schema supports it, so changes stay reversible.
|
||||||
- Package installation should prefer existing WinGet and Chocolatey helper functions.
|
- ISO workflows never modify the user's original ISO file; they work on copied/mounted content.
|
||||||
- ISO workflows must not modify the user's original ISO file; they should work on copied/mounted content following existing patterns.
|
|
||||||
|
## Docs Site (Astro)
|
||||||
|
|
||||||
|
- `docs/` is an Astro + Starlight site, independent of `Compile.ps1`'s build (its own `package.json`/`node_modules`, deployed via the `docs.yaml` GitHub Actions workflow to GitHub Pages).
|
||||||
|
- Pages live under `docs/src/content/docs/` (`.mdx`), organized into `guides/`, `code-reference/`, plus top-level pages like `faq.mdx`, `knownissues.mdx`, `contributing.mdx`, `index.mdx`.
|
||||||
|
- `docs/src/content/docs/code-reference/tweaks/` and `.../features/` are auto-generated by `tools/devdocs-generator.ps1` from `config/tweaks.json`/`config/feature.json` and the relevant PowerShell function files. Other pages under `code-reference/` (e.g. `architecture.mdx`) are hand-written and untouched by the generator.
|
||||||
|
- Sidebar entries in `docs/astro.config.mjs` must match actual page slugs under `docs/src/content/docs/`.
|
||||||
|
- `docs/public/` is tracked source for static assets (favicons, etc.), not generated output. Generated/ignored paths are listed in `docs/.gitignore` (`dist/`, `.astro/`, `node_modules/`, local env files).
|
||||||
|
- `docs/src/assets/branding/title-screen.png` is a tracked generated image used by the repository README and docs homepage. Its raw Light and Dark captures are temporary.
|
||||||
|
- `docs/Dockerfile` and `docs/docker-compose.yml` (service `winutil-astro`) containerize the site's npm tooling; see AGENTS.md's Dependency Installs, Builds, And Dev Servers for why and how agents must use them instead of running npm on the host.
|
||||||
|
|
||||||
## Testing And CI
|
## Testing And CI
|
||||||
|
|
||||||
Expected validation for source changes:
|
|
||||||
|
|
||||||
- `.\Compile.ps1` verifies the compiler can generate `winutil.ps1`.
|
- `.\Compile.ps1` verifies the compiler can generate `winutil.ps1`.
|
||||||
- `.\Compile.ps1 -Run` compiles and launches the generated utility for manual GUI verification.
|
- `.\Compile.ps1 -Run` compiles and launches the generated utility for manual GUI verification.
|
||||||
- `Install-Module -Name Pester -RequiredVersion 5.8.0 -Scope CurrentUser -Force -SkipPublisherCheck` installs the supported Pester version.
|
- Pester 5.8.0 runs the suite under `pester/*.Tests.ps1`. GitHub Actions (`unittests.yaml`) installs Pester 5.8.0 fresh and runs with `-CI`, which produces `testResults.xml` and exits non-zero on failure.
|
||||||
- `Import-Module Pester -RequiredVersion 5.8.0 -Force; Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI` runs the Pester suite.
|
- GitHub Actions also runs PowerShell Script Analyzer with `lint/PSScriptAnalyser.ps1` on every push.
|
||||||
- GitHub Actions also runs a compile check and PowerShell Script Analyzer with `lint/PSScriptAnalyser.ps1`.
|
- The generated `winutil.ps1` may appear locally after compile. It remains ignored build output (see root `.gitignore`) and must not be committed.
|
||||||
|
- The manually triggered title-screen workflow compiles WinUtil from `main` and opens an image-only pull request when the generated composite changes. These pull requests require manual review. Failed runs retain diagnostics for 14 days.
|
||||||
The generated `winutil.ps1` may appear locally after compile. It remains ignored build output and must not be committed.
|
|
||||||
|
|
||||||
## Release Artifact
|
## Release Artifact
|
||||||
|
|
||||||
GitHub Actions is responsible for producing the release `winutil.ps1` from repository sources. A release should be considered valid only if the generated script came from the compile process, not from direct manual edits to `winutil.ps1`.
|
GitHub Actions is responsible for producing the release `winutil.ps1` from repository sources. A release is considered valid only if the generated script came from the compile process, not from direct manual edits to `winutil.ps1`.
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
"winget": "7zip.7zip",
|
"winget": "7zip.7zip",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"abdownloadmanager": {
|
||||||
|
"category": "Utilities",
|
||||||
|
"choco": "ab-download-manager",
|
||||||
|
"content": "AB Download Manager",
|
||||||
|
"description": "AB Download Manager is an open-source download accelerator and manager with multi-threaded downloads, queue scheduling, speed limiting, and browser integration.",
|
||||||
|
"link": "https://abdownloadmanager.com/",
|
||||||
|
"winget": "amir1376.ABDownloadManager",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"adobe": {
|
"adobe": {
|
||||||
"category": "Document",
|
"category": "Document",
|
||||||
"choco": "adobereader",
|
"choco": "adobereader",
|
||||||
@@ -98,6 +107,15 @@
|
|||||||
"winget": "AutoHotkey.AutoHotkey",
|
"winget": "AutoHotkey.AutoHotkey",
|
||||||
"foss": true
|
"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": {
|
"bitwarden": {
|
||||||
"category": "Utilities",
|
"category": "Utilities",
|
||||||
"choco": "bitwarden",
|
"choco": "bitwarden",
|
||||||
@@ -125,6 +143,15 @@
|
|||||||
"winget": "Brave.Brave",
|
"winget": "Brave.Brave",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"bruno": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "bruno",
|
||||||
|
"content": "Bruno",
|
||||||
|
"description": "Bruno is a local-first API client that stores collections as plain text files for version control and collaboration.",
|
||||||
|
"link": "https://www.usebruno.com/",
|
||||||
|
"winget": "Bruno.Bruno",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"bulkcrapuninstaller": {
|
"bulkcrapuninstaller": {
|
||||||
"category": "Utilities",
|
"category": "Utilities",
|
||||||
"choco": "bulk-crap-uninstaller",
|
"choco": "bulk-crap-uninstaller",
|
||||||
@@ -166,7 +193,7 @@
|
|||||||
"choco": "na",
|
"choco": "na",
|
||||||
"content": "ChatGPT Desktop",
|
"content": "ChatGPT Desktop",
|
||||||
"description": "The official ChatGPT desktop app for Windows, distributed through the Microsoft Store.",
|
"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",
|
"winget": "msstore:9NT1R1C2HH7J",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
@@ -193,7 +220,7 @@
|
|||||||
"choco": "chromium",
|
"choco": "chromium",
|
||||||
"content": "Chromium",
|
"content": "Chromium",
|
||||||
"description": "Chromium is the open-source project that serves as the foundation for various web browsers, including Chrome.",
|
"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",
|
"winget": "Hibbiki.Chromium",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -319,10 +346,19 @@
|
|||||||
"choco": "dorion",
|
"choco": "dorion",
|
||||||
"content": "Dorion",
|
"content": "Dorion",
|
||||||
"description": "Tiny alternative Discord client with a smaller footprint, snappier startup, themes, plugins and more!",
|
"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",
|
"winget": "SpikeHD.Dorion",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"dockerdesktop": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "docker-desktop",
|
||||||
|
"content": "Docker Desktop",
|
||||||
|
"description": "Docker Desktop provides a local environment for building, running, and testing containerized applications on Windows.",
|
||||||
|
"link": "https://www.docker.com/products/docker-desktop/",
|
||||||
|
"winget": "Docker.DockerDesktop",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
"dotnet6": {
|
"dotnet6": {
|
||||||
"category": "Microsoft Tools",
|
"category": "Microsoft Tools",
|
||||||
"choco": "dotnet-6.0-runtime",
|
"choco": "dotnet-6.0-runtime",
|
||||||
@@ -395,6 +431,16 @@
|
|||||||
"winget": "Microsoft.Edge",
|
"winget": "Microsoft.Edge",
|
||||||
"foss": false
|
"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": {
|
"enteauth": {
|
||||||
"category": "Utilities",
|
"category": "Utilities",
|
||||||
"choco": "ente-auth",
|
"choco": "ente-auth",
|
||||||
@@ -418,10 +464,19 @@
|
|||||||
"choco": "files",
|
"choco": "files",
|
||||||
"content": "Files",
|
"content": "Files",
|
||||||
"description": "Alternative file explorer.",
|
"description": "Alternative file explorer.",
|
||||||
"link": "https://github.com/files-community/Files",
|
"link": "https://files.community",
|
||||||
"winget": "FilesCommunity.Files",
|
"winget": "FilesCommunity.Files",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"fileconverter": {
|
||||||
|
"category": "Multimedia Tools",
|
||||||
|
"choco": "file-converter",
|
||||||
|
"content": "File Converter",
|
||||||
|
"description": "File Converter converts and compresses files from the Windows Explorer context menu.",
|
||||||
|
"link": "https://file-converter.io/",
|
||||||
|
"winget": "AdrienAllard.FileConverter",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"firefox": {
|
"firefox": {
|
||||||
"category": "Browsers",
|
"category": "Browsers",
|
||||||
"choco": "firefox",
|
"choco": "firefox",
|
||||||
@@ -458,6 +513,24 @@
|
|||||||
"winget": "flux.flux",
|
"winget": "flux.flux",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
|
"foobar": {
|
||||||
|
"category": "Multimedia Tools",
|
||||||
|
"choco": "foobar2000",
|
||||||
|
"content": "foobar2000 (Music Player)",
|
||||||
|
"description": "foobar2000 is a highly customizable and extensible music player for Windows, known for its modular design and advanced features.",
|
||||||
|
"link": "https://www.foobar2000.org/",
|
||||||
|
"winget": "PeterPawlowski.foobar2000",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
|
"fnm": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "fnm",
|
||||||
|
"content": "Fast Node Manager",
|
||||||
|
"description": "Fast Node Manager (fnm) is a fast, cross-platform tool for installing and switching between Node.js versions.",
|
||||||
|
"link": "https://github.com/Schniz/fnm",
|
||||||
|
"winget": "Schniz.fnm",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"foxpdfreader": {
|
"foxpdfreader": {
|
||||||
"category": "Document",
|
"category": "Document",
|
||||||
"choco": "foxitreader",
|
"choco": "foxitreader",
|
||||||
@@ -494,6 +567,24 @@
|
|||||||
"winget": "Git.Git",
|
"winget": "Git.Git",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"gitextensions": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "gitextensions",
|
||||||
|
"content": "Git Extensions",
|
||||||
|
"description": "Git Extensions is a graphical Git client for Windows with repository, history, and commit management tools.",
|
||||||
|
"link": "https://gitextensions.github.io/",
|
||||||
|
"winget": "GitExtensionsTeam.GitExtensions",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
|
"githubcli": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "gh",
|
||||||
|
"content": "GitHub CLI",
|
||||||
|
"description": "GitHub CLI brings pull requests, issues, releases, and other GitHub workflows to the terminal.",
|
||||||
|
"link": "https://cli.github.com/",
|
||||||
|
"winget": "GitHub.cli",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"githubdesktop": {
|
"githubdesktop": {
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"choco": "git;github-desktop",
|
"choco": "git;github-desktop",
|
||||||
@@ -553,7 +644,7 @@
|
|||||||
"choco": "helium",
|
"choco": "helium",
|
||||||
"content": "Helium",
|
"content": "Helium",
|
||||||
"description": "Private, fast, and honest web browser.",
|
"description": "Private, fast, and honest web browser.",
|
||||||
"link": "https://github.com/imputnet/helium/",
|
"link": "https://helium.computer",
|
||||||
"winget": "ImputNet.Helium",
|
"winget": "ImputNet.Helium",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -562,7 +653,7 @@
|
|||||||
"choco": "hugo-extended",
|
"choco": "hugo-extended",
|
||||||
"content": "Hugo",
|
"content": "Hugo",
|
||||||
"description": "The world's fastest framework for building websites.",
|
"description": "The world's fastest framework for building websites.",
|
||||||
"link": "https://github.com/gohugoio/hugo/",
|
"link": "https://gohugo.io",
|
||||||
"winget": "Hugo.Hugo.Extended",
|
"winget": "Hugo.Hugo.Extended",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -679,7 +770,7 @@
|
|||||||
"choco": "jellyfin-media-player",
|
"choco": "jellyfin-media-player",
|
||||||
"content": "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.",
|
"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",
|
"winget": "Jellyfin.JellyfinMediaPlayer",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -769,7 +860,7 @@
|
|||||||
"choco": "librewolf",
|
"choco": "librewolf",
|
||||||
"content": "LibreWolf",
|
"content": "LibreWolf",
|
||||||
"description": "LibreWolf is a privacy-focused web browser based on Firefox, with additional privacy and security enhancements.",
|
"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",
|
"winget": "LibreWolf.LibreWolf",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -787,10 +878,18 @@
|
|||||||
"choco": "mediainfo",
|
"choco": "mediainfo",
|
||||||
"content": "mpc-qt",
|
"content": "mpc-qt",
|
||||||
"description": "Media Player Classic Qute Theater",
|
"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",
|
"winget": "mpc-qt.mpc-qt",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"mpv": {
|
||||||
|
"category": "Multimedia Tools",
|
||||||
|
"content": "mpv",
|
||||||
|
"description": "mpv is a free, open source, and cross-platform media player supporting a wide variety of media formats, codecs, and subtitle types.",
|
||||||
|
"link": "https://mpv.io/",
|
||||||
|
"winget": "shinchiro.mpv",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"matrix": {
|
"matrix": {
|
||||||
"category": "Communications",
|
"category": "Communications",
|
||||||
"choco": "element-desktop",
|
"choco": "element-desktop",
|
||||||
@@ -832,7 +931,7 @@
|
|||||||
"choco": "mpc-hc-clsid2",
|
"choco": "mpc-hc-clsid2",
|
||||||
"content": "Media Player Classic - Home Cinema",
|
"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.",
|
"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",
|
"winget": "clsid2.mpc-hc",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -859,7 +958,7 @@
|
|||||||
"choco": "mullvad-app",
|
"choco": "mullvad-app",
|
||||||
"content": "Mullvad VPN",
|
"content": "Mullvad VPN",
|
||||||
"description": "This is the VPN client software for the Mullvad VPN service.",
|
"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",
|
"winget": "MullvadVPN.MullvadVPN",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -886,7 +985,7 @@
|
|||||||
"choco": "nanazip",
|
"choco": "nanazip",
|
||||||
"content": "NanaZip",
|
"content": "NanaZip",
|
||||||
"description": "NanaZip is a fast and efficient file compression and decompression tool.",
|
"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",
|
"winget": "M2Team.NanaZip",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -899,6 +998,15 @@
|
|||||||
"winget": "Netbird.Netbird",
|
"winget": "Netbird.Netbird",
|
||||||
"foss": true
|
"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": {
|
"naps2": {
|
||||||
"category": "Document",
|
"category": "Document",
|
||||||
"choco": "naps2",
|
"choco": "naps2",
|
||||||
@@ -1186,6 +1294,15 @@
|
|||||||
"winget": "JanDeDobbeleer.OhMyPosh",
|
"winget": "JanDeDobbeleer.OhMyPosh",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"postman": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "postman",
|
||||||
|
"content": "Postman",
|
||||||
|
"description": "Postman is an API platform and desktop client for designing, testing, documenting, and collaborating on APIs.",
|
||||||
|
"link": "https://www.postman.com/downloads/",
|
||||||
|
"winget": "Postman.Postman",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
"powershell": {
|
"powershell": {
|
||||||
"category": "Microsoft Tools",
|
"category": "Microsoft Tools",
|
||||||
"choco": "powershell-core",
|
"choco": "powershell-core",
|
||||||
@@ -1303,6 +1420,15 @@
|
|||||||
"winget": "qBittorrent.qBittorrent",
|
"winget": "qBittorrent.qBittorrent",
|
||||||
"foss": true
|
"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": {
|
"qtox": {
|
||||||
"category": "Communications",
|
"category": "Communications",
|
||||||
"choco": "qtox",
|
"choco": "qtox",
|
||||||
@@ -1438,6 +1564,15 @@
|
|||||||
"winget": "StartIsBack.StartAllBack",
|
"winget": "StartIsBack.StartAllBack",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
|
"starship": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "starship",
|
||||||
|
"content": "Starship (Shell Prompt)",
|
||||||
|
"description": "Starship is a fast, customizable, cross-platform prompt for PowerShell and other shells.",
|
||||||
|
"link": "https://starship.rs/",
|
||||||
|
"winget": "Starship.Starship",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"steam": {
|
"steam": {
|
||||||
"category": "Games",
|
"category": "Games",
|
||||||
"choco": "steam-client",
|
"choco": "steam-client",
|
||||||
@@ -1447,6 +1582,15 @@
|
|||||||
"winget": "Valve.Steam",
|
"winget": "Valve.Steam",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
|
"roblox": {
|
||||||
|
"category": "Games",
|
||||||
|
"choco": "na",
|
||||||
|
"content": "Roblox",
|
||||||
|
"description": "Roblox is a platform and game creation system that allows users to create and play games developed by the community.",
|
||||||
|
"link": "https://www.roblox.com/",
|
||||||
|
"winget": "Roblox.Roblox",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
"sublimetext": {
|
"sublimetext": {
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"choco": "sublimetext4",
|
"choco": "sublimetext4",
|
||||||
@@ -1470,10 +1614,27 @@
|
|||||||
"choco": "sunshine",
|
"choco": "sunshine",
|
||||||
"content": "Sunshine/GameStream Server",
|
"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.",
|
"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",
|
"winget": "LizardByte.Sunshine",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
|
"synctrayzor": {
|
||||||
|
"category": "Selfhosted Tools",
|
||||||
|
"choco": "synctrayzor",
|
||||||
|
"content": "SyncTrayzor",
|
||||||
|
"description": "SyncTrayzor is a Windows tray utility that bundles and wraps Syncthing, making it behave like a native application to easily manage and monitor file synchronization.",
|
||||||
|
"link": "https://github.com/GermanCoding/SyncTrayzor",
|
||||||
|
"winget": "GermanCoding.SyncTrayzor"
|
||||||
|
},
|
||||||
|
"syncthing": {
|
||||||
|
"category": "Selfhosted Tools",
|
||||||
|
"choco": "syncthing",
|
||||||
|
"content": "Syncthing (CLI / Web UI)",
|
||||||
|
"description": "Syncthing is a decentralized, peer-to-peer file synchronization tool that securely syncs files directly across devices without cloud servers, managed via a local web interface.",
|
||||||
|
"link": "https://syncthing.net/",
|
||||||
|
"winget": "Syncthing.Syncthing",
|
||||||
|
"foss": true
|
||||||
|
},
|
||||||
"tcpview": {
|
"tcpview": {
|
||||||
"category": "Microsoft Tools",
|
"category": "Microsoft Tools",
|
||||||
"choco": "tcpview",
|
"choco": "tcpview",
|
||||||
@@ -1510,6 +1671,15 @@
|
|||||||
"winget": "TeamSpeakSystems.TeamSpeakClient",
|
"winget": "TeamSpeakSystems.TeamSpeakClient",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
|
"teamspeak6": {
|
||||||
|
"category": "Communications",
|
||||||
|
"choco": "na",
|
||||||
|
"content": "TeamSpeak 6",
|
||||||
|
"description": "TEAMSPEAK. YOUR TEAM. YOUR RULES. Use crystal clear sound to communicate with your teammates cross-platform with military-grade security, lag-free performance & unparalleled reliability and uptime.",
|
||||||
|
"link": "https://www.teamspeak.com/",
|
||||||
|
"winget": "TeamSpeakSystems.TeamSpeakClient.Beta.6",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"category": "Communications",
|
"category": "Communications",
|
||||||
"choco": "telegram",
|
"choco": "telegram",
|
||||||
@@ -1578,7 +1748,7 @@
|
|||||||
"choco": "translucenttb",
|
"choco": "translucenttb",
|
||||||
"content": "TranslucentTB",
|
"content": "TranslucentTB",
|
||||||
"description": "TranslucentTB is a tool that allows you to customize the transparency of the Windows Taskbar.",
|
"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",
|
"winget": "CharlesMilette.TranslucentTB",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -1609,6 +1779,15 @@
|
|||||||
"winget": "Unity.UnityHub",
|
"winget": "Unity.UnityHub",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
|
"vagrant": {
|
||||||
|
"category": "Development",
|
||||||
|
"choco": "vagrant",
|
||||||
|
"content": "Vagrant",
|
||||||
|
"description": "Vagrant builds and manages reproducible virtual machine development environments from declarative configuration.",
|
||||||
|
"link": "https://developer.hashicorp.com/vagrant",
|
||||||
|
"winget": "Hashicorp.Vagrant",
|
||||||
|
"foss": false
|
||||||
|
},
|
||||||
"everything": {
|
"everything": {
|
||||||
"category": "Utilities",
|
"category": "Utilities",
|
||||||
"choco": "everything",
|
"choco": "everything",
|
||||||
@@ -1649,8 +1828,8 @@
|
|||||||
"category": "Communications",
|
"category": "Communications",
|
||||||
"choco": "na",
|
"choco": "na",
|
||||||
"content": "Vesktop",
|
"content": "Vesktop",
|
||||||
"description": "A cross platform electron-based desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.",
|
"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",
|
"winget": "Vencord.Vesktop",
|
||||||
"foss": true
|
"foss": true
|
||||||
},
|
},
|
||||||
@@ -1740,7 +1919,7 @@
|
|||||||
"choco": "na",
|
"choco": "na",
|
||||||
"content": "WhatsApp Desktop",
|
"content": "WhatsApp Desktop",
|
||||||
"description": "WhatsApp Desktop is the official Windows desktop messaging app from Meta, distributed through the Microsoft Store.",
|
"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",
|
"winget": "msstore:9NKSQGP7F2NH",
|
||||||
"foss": false
|
"foss": false
|
||||||
},
|
},
|
||||||
@@ -1863,9 +2042,8 @@
|
|||||||
},
|
},
|
||||||
"Overwolf": {
|
"Overwolf": {
|
||||||
"category": "Games",
|
"category": "Games",
|
||||||
"choco": "overwolf",
|
"content": "CurseForge",
|
||||||
"content": "Overwolf",
|
"description": "CurseForge is a desktop application for managing mods and modpacks across multiple games, powered by Overwolf.",
|
||||||
"description": "Popular platform for game overlays and companion apps (mod managers, trackers, etc.), widely used by gamers.",
|
|
||||||
"link": "https://www.overwolf.com/app/overwolf-curseforge",
|
"link": "https://www.overwolf.com/app/overwolf-curseforge",
|
||||||
"winget": "Overwolf.CurseForge",
|
"winget": "Overwolf.CurseForge",
|
||||||
"foss": false
|
"foss": false
|
||||||
@@ -1932,5 +2110,15 @@
|
|||||||
"content": "Lua",
|
"content": "Lua",
|
||||||
"link": "https://github.com/rjpcomputing/luaforwindows",
|
"link": "https://github.com/rjpcomputing/luaforwindows",
|
||||||
"foss": true
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,5 +259,13 @@
|
|||||||
"Description": "Bundles built-in card game modes including Klondike, Spider, FreeCell, Pyramid, and TriPeaks alongside daily challenges.",
|
"Description": "Bundles built-in card game modes including Klondike, Spider, FreeCell, Pyramid, and TriPeaks alongside daily challenges.",
|
||||||
"Panel": "1",
|
"Panel": "1",
|
||||||
"PackageId": "Microsoft.MicrosoftSolitaireCollection"
|
"PackageId": "Microsoft.MicrosoftSolitaireCollection"
|
||||||
|
},
|
||||||
|
"WPFAppxMicrosoft_ZuneVideo": {
|
||||||
|
"Category": "Utilities & Productivity",
|
||||||
|
"Content": "Movies & TV",
|
||||||
|
"Description": "The default video player and storefront for purchasing or renting media.",
|
||||||
|
"Panel": "0",
|
||||||
|
"PackageId": "Microsoft.ZuneVideo",
|
||||||
|
"StoreId": "9WZDNCRFJ3P2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Google":{
|
"Google":{
|
||||||
|
"BenchmarkEligible": true,
|
||||||
"Primary": "8.8.8.8",
|
"Primary": "8.8.8.8",
|
||||||
"Secondary": "8.8.4.4",
|
"Secondary": "8.8.4.4",
|
||||||
"Primary6": "2001:4860:4860::8888",
|
"Primary6": "2001:4860:4860::8888",
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
"DohTemplate": "https://dns.google/dns-query"
|
"DohTemplate": "https://dns.google/dns-query"
|
||||||
},
|
},
|
||||||
"Cloudflare":{
|
"Cloudflare":{
|
||||||
|
"BenchmarkEligible": true,
|
||||||
"Primary": "1.1.1.1",
|
"Primary": "1.1.1.1",
|
||||||
"Secondary": "1.0.0.1",
|
"Secondary": "1.0.0.1",
|
||||||
"Primary6": "2606:4700:4700::1111",
|
"Primary6": "2606:4700:4700::1111",
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
"FontSize": "12",
|
"FontSize": "12",
|
||||||
"FontFamily": "Arial",
|
"FontFamily": "Arial",
|
||||||
"HeaderFontSize": "16",
|
"HeaderFontSize": "16",
|
||||||
|
"Win11StepTitleFontSize": "22",
|
||||||
|
"Win11StepHeroFontSize": "40",
|
||||||
|
"Win11LogFontFamily": "Consolas, Monaco",
|
||||||
"HeaderFontFamily": "Consolas, Monaco",
|
"HeaderFontFamily": "Consolas, Monaco",
|
||||||
"CheckBoxBulletDecoratorSize": "14",
|
"CheckBoxBulletDecoratorSize": "14",
|
||||||
"CheckBoxMargin": "15,0,0,2",
|
"CheckBoxMargin": "15,0,0,2",
|
||||||
@@ -58,6 +61,8 @@
|
|||||||
"ScrollBarHoverColor": "#5A5D62",
|
"ScrollBarHoverColor": "#5A5D62",
|
||||||
"ScrollBarDraggingColor": "#6A6D72",
|
"ScrollBarDraggingColor": "#6A6D72",
|
||||||
"ProgressBarForegroundColor": "#2E77FF",
|
"ProgressBarForegroundColor": "#2E77FF",
|
||||||
|
"ProgressBarErrorColor": "#D13438",
|
||||||
|
"ProgressBarWarningColor": "#B36A00",
|
||||||
"ProgressBarBackgroundColor": "Transparent",
|
"ProgressBarBackgroundColor": "Transparent",
|
||||||
"ButtonInstallBackgroundColor": "#F7F7F7",
|
"ButtonInstallBackgroundColor": "#F7F7F7",
|
||||||
"ButtonTweaksBackgroundColor": "#F7F7F7",
|
"ButtonTweaksBackgroundColor": "#F7F7F7",
|
||||||
@@ -98,6 +103,8 @@
|
|||||||
"ScrollBarHoverColor": "#3B4252",
|
"ScrollBarHoverColor": "#3B4252",
|
||||||
"ScrollBarDraggingColor": "#5E81AC",
|
"ScrollBarDraggingColor": "#5E81AC",
|
||||||
"ProgressBarForegroundColor": "#6EFF72",
|
"ProgressBarForegroundColor": "#6EFF72",
|
||||||
|
"ProgressBarErrorColor": "#FF6B6B",
|
||||||
|
"ProgressBarWarningColor": "#FFC83D",
|
||||||
"ProgressBarBackgroundColor": "Transparent",
|
"ProgressBarBackgroundColor": "Transparent",
|
||||||
"ButtonInstallBackgroundColor": "#222222",
|
"ButtonInstallBackgroundColor": "#222222",
|
||||||
"ButtonTweaksBackgroundColor": "#333333",
|
"ButtonTweaksBackgroundColor": "#333333",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"WPFTweaksActivity": {
|
"WPFTweaksActivity": {
|
||||||
"Content": "Activity History - Disable",
|
"Content": "Activity History - Disable",
|
||||||
"Description": "Erases recent docs, clipboard, and run history.",
|
"Description": "Stops Windows from publishing or uploading user activities while preserving clipboard history.",
|
||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"registry": [
|
"registry": [
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
|
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
|
||||||
"Name": "EnableActivityFeed",
|
"Name": "EnableActivityFeed",
|
||||||
"Value": "0",
|
"Value": "1",
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "<RemoveEntry>"
|
"OriginalValue": "<RemoveEntry>"
|
||||||
},
|
},
|
||||||
@@ -99,10 +99,10 @@
|
|||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny Everyone:F"
|
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny *S-1-1-0:F"
|
||||||
],
|
],
|
||||||
"UndoScript": [
|
"UndoScript": [
|
||||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant Everyone:F"
|
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant *S-1-1-0:F"
|
||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/essential-tweaks/disablestoresearch"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/essential-tweaks/disablestoresearch"
|
||||||
},
|
},
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"service": [
|
"service": [
|
||||||
{
|
{
|
||||||
"Name": "lfsvc",
|
"Name": "lfsvc",
|
||||||
"StartupType": "Disable",
|
"StartupType": "Disabled",
|
||||||
"OriginalType": "Manual"
|
"OriginalType": "Manual"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -647,7 +647,7 @@
|
|||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"
|
"
|
||||||
# Deny permission to remove OneDrive folder
|
# Deny permission to remove OneDrive folder
|
||||||
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
|
icacls $Env:OneDrive /deny \"*S-1-5-32-544:(D,DC)\"
|
||||||
|
|
||||||
Write-Host \"Uninstalling OneDrive...\"
|
Write-Host \"Uninstalling OneDrive...\"
|
||||||
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
|
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
|
||||||
@@ -661,7 +661,7 @@
|
|||||||
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
|
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
|
||||||
|
|
||||||
# Grant back permission to access OneDrive folder
|
# Grant back permission to access OneDrive folder
|
||||||
icacls $Env:OneDrive /grant \"Administrators:(D,DC)\"
|
icacls $Env:OneDrive /grant \"*S-1-5-32-544:(D,DC)\"
|
||||||
|
|
||||||
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
|
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
|
||||||
Remove-Item -Path $Env:OneDrive -Recurse
|
Remove-Item -Path $Env:OneDrive -Recurse
|
||||||
@@ -854,7 +854,7 @@
|
|||||||
},
|
},
|
||||||
"WPFTweaksEndTaskOnTaskbar": {
|
"WPFTweaksEndTaskOnTaskbar": {
|
||||||
"Content": "End Task With Right Click - Enable",
|
"Content": "End Task With Right Click - Enable",
|
||||||
"Description": "Enables option to end task when right clicking a program in the taskbar.",
|
"Description": "Enables option to end task when right-clicking a program in the taskbar.",
|
||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"registry": [
|
"registry": [
|
||||||
@@ -991,16 +991,51 @@
|
|||||||
New-Item -Path $RazerPath -ItemType Directory
|
New-Item -Path $RazerPath -ItemType Directory
|
||||||
}
|
}
|
||||||
|
|
||||||
icacls $RazerPath /deny \"Everyone:(W)\"
|
icacls $RazerPath /deny \"*S-1-1-0:(W)\"
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
"UndoScript": [
|
"UndoScript": [
|
||||||
"
|
"
|
||||||
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d Everyone
|
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d *S-1-1-0
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/razerblock"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/razerblock"
|
||||||
},
|
},
|
||||||
|
"WPFTweaksLogiBlock": {
|
||||||
|
"Content": "Logitech Download Assistant Auto-Install - Disable",
|
||||||
|
"Description": "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it.",
|
||||||
|
"category": "z__Advanced Tweaks - CAUTION",
|
||||||
|
"panel": "1",
|
||||||
|
"InvokeScript": [
|
||||||
|
"
|
||||||
|
Stop-Process -Name \"logi_download_assistant\" -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||||
|
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||||
|
|
||||||
|
if (Test-Path $LogiPath) {
|
||||||
|
Remove-Item $LogiPath\\* -Recurse -Force
|
||||||
|
} else {
|
||||||
|
New-Item -Path $LogiPath -ItemType Directory
|
||||||
|
}
|
||||||
|
|
||||||
|
icacls $LogiPath /deny \"*S-1-1-0:(W)\"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to deny write access on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||||
|
"
|
||||||
|
],
|
||||||
|
"UndoScript": [
|
||||||
|
"
|
||||||
|
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||||
|
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||||
|
|
||||||
|
if (Test-Path $LogiPath) {
|
||||||
|
icacls $LogiPath /remove:d \"*S-1-1-0\"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to remove the write-deny rule on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||||
|
}
|
||||||
|
"
|
||||||
|
],
|
||||||
|
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/logiblock"
|
||||||
|
},
|
||||||
"WPFTweaksDisableNotifications": {
|
"WPFTweaksDisableNotifications": {
|
||||||
"Content": "System Tray Notifications & Calendar - Disable",
|
"Content": "System Tray Notifications & Calendar - Disable",
|
||||||
"Description": "Disables all Notifications INCLUDING Calendar.",
|
"Description": "Disables all Notifications INCLUDING Calendar.",
|
||||||
@@ -1086,8 +1121,10 @@
|
|||||||
"panel": "1",
|
"panel": "1",
|
||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"
|
"
|
||||||
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force
|
# A temp folder always holds files something has open, including this run's own, and
|
||||||
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force
|
# the job layer counts a logged error as a failed step
|
||||||
|
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/essential-tweaks/deletetempfiles"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/essential-tweaks/deletetempfiles"
|
||||||
@@ -1168,22 +1205,6 @@
|
|||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/disablebgapps"
|
"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": {
|
"WPFTweaksDisableExplorerAutoDiscovery": {
|
||||||
"Content": "File Explorer Automatic Folder Discovery - Disable",
|
"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.",
|
"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.",
|
||||||
@@ -1401,7 +1422,7 @@
|
|||||||
},
|
},
|
||||||
"WPFToggleNewOutlook": {
|
"WPFToggleNewOutlook": {
|
||||||
"Content": "Microsoft Outlook New Version",
|
"Content": "Microsoft Outlook New Version",
|
||||||
"Description": "This will ensures the classic Outlook application is used.",
|
"Description": "This will ensure the new Outlook application is used.",
|
||||||
"category": "Customize Preferences",
|
"category": "Customize Preferences",
|
||||||
"panel": "2",
|
"panel": "2",
|
||||||
"Type": "Toggle",
|
"Type": "Toggle",
|
||||||
@@ -1459,28 +1480,40 @@
|
|||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/scrollbars"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/scrollbars"
|
||||||
},
|
},
|
||||||
"WPFToggleMultiplaneOverlay": {
|
"WPFMultiplaneOverlay": {
|
||||||
"Content": "Multiplane Overlay",
|
"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",
|
"category": "Customize Preferences",
|
||||||
"panel": "2",
|
"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": [
|
"registry": [
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\Dwm",
|
"Path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\Dwm",
|
||||||
"Name": "OverlayTestMode",
|
"Name": "OverlayTestMode",
|
||||||
"Value": "0",
|
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "5",
|
"DefaultValue": "0",
|
||||||
"DefaultState": "true"
|
"Values": {
|
||||||
|
"Enabled": "<RemoveEntry>",
|
||||||
|
"Disabled (Compatibility)": "5",
|
||||||
|
"Fully Disabled": "5"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers",
|
"Path": "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers",
|
||||||
"Name": "DisableOverlays",
|
"Name": "DisableOverlays",
|
||||||
"Value": "0",
|
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "1",
|
"DefaultValue": "0",
|
||||||
"DefaultState": "true"
|
"Values": {
|
||||||
|
"Enabled": "<RemoveEntry>",
|
||||||
|
"Disabled (Compatibility)": "<RemoveEntry>",
|
||||||
|
"Fully Disabled": "1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/multiplaneoverlay"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/multiplaneoverlay"
|
||||||
@@ -1653,7 +1686,7 @@
|
|||||||
],
|
],
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/loginblur"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/customize-preferences/loginblur"
|
||||||
},
|
},
|
||||||
"WPFTweaksDisableLockscreen": {
|
"WPFToggleDisableLockscreen": {
|
||||||
"Content": "Lock Screen - Disable",
|
"Content": "Lock Screen - Disable",
|
||||||
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
|
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
|
||||||
"category": "Customize Preferences",
|
"category": "Customize Preferences",
|
||||||
@@ -1852,7 +1885,7 @@
|
|||||||
"category": "z__Advanced Tweaks - CAUTION",
|
"category": "z__Advanced Tweaks - CAUTION",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"Type": "Combobox",
|
"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 Fastest Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult",
|
||||||
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/changedns"
|
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/changedns"
|
||||||
},
|
},
|
||||||
"WPFAddUltPerf": {
|
"WPFAddUltPerf": {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.astro
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.log
|
||||||
@@ -16,6 +16,8 @@ pnpm-debug.log*
|
|||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 4321
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
@@ -1,26 +1,24 @@
|
|||||||
# Starlight Starter Kit: Basics
|
# WinUtil Docs
|
||||||
|
|
||||||
[](https://starlight.astro.build)
|
[](https://starlight.astro.build)
|
||||||
|
|
||||||
```
|
Documentation site for [WinUtil](https://github.com/ChrisTitusTech/winutil), built with [Astro](https://astro.build) and [Starlight](https://starlight.astro.build). Served at [winutil.christitus.com](https://winutil.christitus.com/).
|
||||||
npm create astro@latest -- --template starlight
|
|
||||||
```
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
## 🚀 Project Structure
|
||||||
|
|
||||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── public/
|
├── public/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── assets/
|
│ ├── assets/
|
||||||
|
│ ├── components/
|
||||||
│ ├── content/
|
│ ├── content/
|
||||||
│ │ └── docs/
|
│ │ └── docs/
|
||||||
|
│ ├── styles/
|
||||||
│ └── content.config.ts
|
│ └── content.config.ts
|
||||||
├── astro.config.mjs
|
├── astro.config.mjs
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
├── package.json
|
├── package.json
|
||||||
└── tsconfig.json
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
@@ -33,17 +31,31 @@ Static assets, like favicons, can be placed in the `public/` directory.
|
|||||||
|
|
||||||
## 🧞 Commands
|
## 🧞 Commands
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
All commands run in a Docker container — there's no need to install Node or npm dependencies on your host. This is deliberate, not just convenience: npm/pnpm/yarn have seen a steady stream of supply-chain attacks (malicious `postinstall`/`preinstall` scripts, credential-stealing packages), so `npm install` and friends never run directly on a contributor's machine here. Note the container still has read-write access to this `docs/` directory (it's bind-mounted for live reload), so this only contains a compromised package to the project folder plus the container itself — it doesn't reach the rest of your host (SSH keys, other repos, cloud credentials elsewhere on disk). Don't keep real secrets in `docs/` as a result.
|
||||||
|
|
||||||
| Command | Action |
|
[Docker](https://www.docker.com/) (with Compose) is required — install Docker Desktop (or Docker Engine + the `docker compose` plugin on Linux) and make sure the daemon is running before using any of the commands below.
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
All commands are run from the `docs/` directory, from a terminal:
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
| Command | Action |
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
| :------------------------------------------------ | :----------------------------------------------- |
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
| `docker compose build` | Builds the dev image (needed after Dockerfile or dependency changes) |
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
| `docker compose up winutil-astro` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `docker compose run --rm winutil-astro npm run build` | Build the production site to `./dist/` |
|
||||||
|
| `docker compose run --rm --service-ports winutil-astro npm run preview -- --host 0.0.0.0` | Preview the build locally, before deploying |
|
||||||
|
| `docker compose run --rm winutil-astro npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `docker compose down` | Stop and remove the dev container |
|
||||||
|
|
||||||
|
Source files are bind-mounted into the container, so edits on the host are picked up immediately by the dev server — no rebuild needed for normal content or code changes. After changing `package.json`, `package-lock.json`, or the `Dockerfile`, rebuild the image *and* drop the `node_modules` volume, since Docker only seeds a named volume from the image the first time it's created — a plain rebuild leaves the old `node_modules` in place:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose build
|
||||||
|
docker compose down -v
|
||||||
|
docker compose up winutil-astro
|
||||||
|
```
|
||||||
|
|
||||||
|
The first `docker compose up` (or any command before an image exists) builds the image and runs `npm install` from scratch, which can take a few minutes. Subsequent runs reuse the cached image and start almost immediately.
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
Check out [Starlight's docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export default defineConfig({
|
|||||||
replacesTitle: true,
|
replacesTitle: true,
|
||||||
},
|
},
|
||||||
favicon: '/favicon.svg',
|
favicon: '/favicon.svg',
|
||||||
|
head: [
|
||||||
|
{ tag: 'meta', attrs: { property: 'og:image', content: 'https://winutil.christitus.com/social-preview.png' } },
|
||||||
|
{ tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } },
|
||||||
|
{ tag: 'meta', attrs: { property: 'og:image:height', content: '630' } },
|
||||||
|
{ tag: 'meta', attrs: { name: 'twitter:image', content: 'https://winutil.christitus.com/social-preview.png' } },
|
||||||
|
],
|
||||||
social: [
|
social: [
|
||||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/ChrisTitusTech/winutil' },
|
{ icon: 'github', label: 'GitHub', href: 'https://github.com/ChrisTitusTech/winutil' },
|
||||||
{ icon: 'discord', label: 'Discord', href: 'https://discord.gg/RUbZUZyByQ' },
|
{ icon: 'discord', label: 'Discord', href: 'https://discord.gg/RUbZUZyByQ' },
|
||||||
@@ -50,6 +56,7 @@ export default defineConfig({
|
|||||||
label: 'Code Reference',
|
label: 'Code Reference',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Architecture & Design', slug: 'code-reference/architecture' },
|
{ 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: 'Tweaks Reference', items: [{ autogenerate: { directory: 'code-reference/tweaks' } }] },
|
||||||
{ label: 'Features Reference', items: [{ autogenerate: { directory: 'code-reference/features' } }] },
|
{ label: 'Features Reference', items: [{ autogenerate: { directory: 'code-reference/features' } }] },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
winutil-astro:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:4321:4321"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- astro_node_modules:/app/node_modules
|
||||||
|
tmpfs:
|
||||||
|
- /app/.astro
|
||||||
|
environment:
|
||||||
|
- CHOKIDAR_USEPOLLING=true
|
||||||
|
- ASTRO_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
astro_node_modules:
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"@astrojs/starlight": "^0.41.5",
|
"@astrojs/starlight": "^0.41.5",
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
"@fontsource/geist-sans": "^5.3.0",
|
"@fontsource/geist-sans": "^5.3.0",
|
||||||
"astro": "^7.0.2",
|
"astro": "^7.3.2",
|
||||||
"sharp": "^0.35.3"
|
"sharp": "^0.35.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://winutil.christitus.com/sitemap-index.xml
|
||||||
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 78 KiB |
@@ -79,14 +79,15 @@ winutil/
|
|||||||
**Why**: Makes distribution easier (single file) and improves load time.
|
**Why**: Makes distribution easier (single file) and improves load time.
|
||||||
|
|
||||||
#### 2. scripts/main.ps1
|
#### 2. scripts/main.ps1
|
||||||
**Purpose**: Entry point that initializes the GUI and event system.
|
**Purpose**: Entry point that manages the run.
|
||||||
|
|
||||||
**Responsibilities**:
|
**Responsibilities**:
|
||||||
- Load XAML and create WPF window
|
|
||||||
- Initialize form elements
|
|
||||||
- Set up event handlers
|
|
||||||
- Load configurations
|
- Load configurations
|
||||||
- Display the GUI
|
- Run the headless `-Preset` and `-Config` paths
|
||||||
|
- Start the interface on a dedicated STA runspace and wait for it
|
||||||
|
- Report anything the interface thread failed with, then clean up
|
||||||
|
|
||||||
|
The interface itself lives in `Start-WinUtilUserInterface`, not here. See [Threading Model](#threading-model).
|
||||||
|
|
||||||
#### 3. functions/public/
|
#### 3. functions/public/
|
||||||
**Purpose**: User-facing functions that implement main features.
|
**Purpose**: User-facing functions that implement main features.
|
||||||
@@ -129,28 +130,36 @@ winutil/
|
|||||||
|
|
||||||
## Win11 Creator Architecture
|
## Win11 Creator Architecture
|
||||||
|
|
||||||
The **Win11 Creator** is a specialized subsystem within Winutil that creates customized Windows 11 ISOs. It operates independently from the main package installation and tweak system.
|
The **Win11 Creator** is a specialized subsystem within Winutil that creates customized Windows 11 ISOs. It operates independently of the main package installation and tweak system.
|
||||||
|
|
||||||
### Win11 Creator Components
|
### Win11 Creator Components
|
||||||
|
|
||||||
**Core Functions** (`functions/private/`):
|
**Core Functions** (`functions/private/`):
|
||||||
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing all Win11 Creator functions
|
- `Invoke-WinUtilISO.ps1`: Main orchestrator containing ISO workflow and helper functions
|
||||||
- `Invoke-WinUtilISOBrowse`: ISO file selection dialog
|
- `Invoke-WinUtilISOBrowse`: ISO file selection dialog
|
||||||
- `Invoke-WinUtilISOMountAndVerify`: Validates and mounts ISO, verifies it's official Windows 11
|
- `Invoke-WinUtilISOMountAndVerify`: Validates and mounts ISO, verifies it is an official Windows 11 ISO
|
||||||
- `Invoke-WinUtilISOModify`: Launches modification in background runspace
|
- `Invoke-WinUtilISOModify`: Launches modification in background runspace
|
||||||
- `Invoke-WinUtilISOExport`: Handles ISO and USB export
|
- `Invoke-WinUtilISOExport`: Builds bootable ISO via `oscdimg.exe`
|
||||||
- `Invoke-WinUtilISOCheckExistingWork`: Recovers incomplete work sessions
|
- `Invoke-WinUtilISOCheckExistingWork`: Recovers incomplete work sessions
|
||||||
- `Invoke-WinUtilISOCleanAndReset`: Cleans up temp directories and resets UI
|
- `Invoke-WinUtilISOCleanAndReset`: Cleans up temp directories and resets UI
|
||||||
|
- `Write-WinUtilISOLog`: Posts log messages to UI status box and session log
|
||||||
|
- `Set-WinUtilISOStep`: Controls wizard navigation state
|
||||||
|
- `Get-WinUtilEditionIdFromName`: Maps edition display names to setup edition IDs
|
||||||
|
- `Invoke-WinUtilRobocopy`: Copies files with exit code verification
|
||||||
|
- `Find-WinUtilOscdimg` / `Get-WinUtilOscdimgPath`: Locates or installs `oscdimg.exe`
|
||||||
|
|
||||||
- `Invoke-WinUtilISOScript.ps1`: Applies modifications to mounted install.wim
|
- `Invoke-WinUtilISOUSB.ps1`: USB drive detection and formatting
|
||||||
- Removes provisioned AppX packages (40+ bloatware apps)
|
- `Invoke-WinUtilISORefreshUSBDrives`: Enumerates USB drives
|
||||||
- Injects drivers (optional) from the current system
|
- `Get-WinUtilFreeDriveLetter`: Finds available drive letters
|
||||||
- Removes OneDrive setup files
|
- `Invoke-WinUtilISOWriteUSB`: Formats USB drive as GPT/FAT32, splits WIM if needed, and copies files
|
||||||
- Applies offline registry tweaks (hardware bypass, privacy, telemetry, OOBE)
|
|
||||||
- Deletes telemetry scheduled task definitions
|
- `Invoke-WinUtilISOScript.ps1`: Prepares setup media and customizations
|
||||||
- Pre-stages setup scripts from autounattend.xml
|
- Stages AppX removal (19 bloatware packages) into `autounattend.xml` for first logon
|
||||||
- Removes unused Windows editions
|
- Applies 50+ registry tweaks during Windows Setup and first logon; removes scheduled tasks at first logon
|
||||||
- Cleans component store via DISM
|
- Triggers OneDrive uninstall during first logon
|
||||||
|
- Stages setup script fallbacks to `sources\$OEM$\$$\Setup\Scripts\`
|
||||||
|
- Pins selected edition in `autounattend.xml` and writes `sources\ei.cfg`
|
||||||
|
- Injects eligible current system drivers into WIM images if enabled (WIM mount is only used for driver servicing)
|
||||||
|
|
||||||
### Win11 Creator Data Flow
|
### Win11 Creator Data Flow
|
||||||
|
|
||||||
@@ -170,36 +179,38 @@ User optionally enables the Driver Injection checkbox
|
|||||||
↓
|
↓
|
||||||
Invoke-WinUtilISOModify (runs in background runspace)
|
Invoke-WinUtilISOModify (runs in background runspace)
|
||||||
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
|
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
|
||||||
├─ Copy ISO contents to disk (~5-6 GB)
|
├─ Copy ISO contents to disk via robocopy (~5-6 GB)
|
||||||
├─ Mount install.wim at selected edition/index
|
|
||||||
├─ Invoke-WinUtilISOScript:
|
├─ Invoke-WinUtilISOScript:
|
||||||
│ ├─ Remove 40+ bloat AppX packages
|
│ ├─ Generate autounattend.xml with Windows PE and specialize safeguards
|
||||||
│ ├─ Export and inject drivers (if enabled)
|
│ ├─ Add first-logon script (19 AppX removals, registry tweaks, OneDrive uninstall)
|
||||||
│ ├─ Remove OneDrive setup
|
│ ├─ Pin selected edition index in autounattend.xml (/IMAGE/INDEX)
|
||||||
│ ├─ Load offline registry hives
|
│ ├─ Stage setup script fallbacks to sources\$OEM$\$$\Setup\Scripts\
|
||||||
│ ├─ Apply 50+ registry tweaks (hardware bypass, privacy, telemetry, OOBE, etc.)
|
│ ├─ Write sources\ei.cfg and remove stale sources\PID.txt
|
||||||
│ ├─ Delete telemetry scheduled task files
|
│ └─ If driver injection enabled:
|
||||||
│ ├─ Pre-stage setup scripts from autounattend.xml to C:\Windows\Setup\Scripts\
|
│ ├─ Require install.wim; install.esd cannot accept driver injection
|
||||||
│ └─ Unload registry hives
|
│ ├─ Export current system drivers via DISM
|
||||||
├─ DISM /Cleanup-Image /StartComponentCleanup /ResetBase (saves 300-800 MB)
|
│ ├─ Exclude stale duplicate packages
|
||||||
├─ Dismount and save the modified install.wim (~10+ minutes, slowest step)
|
│ ├─ Inject storage drivers into boot.wim index 2 and eligible drivers into install.wim
|
||||||
├─ Export selected edition only (removes all other editions, saves 1-2 GB each)
|
│ └─ Validate WIM metadata before and after injection
|
||||||
├─ Dismount source ISO
|
├─ Dismount source ISO
|
||||||
└─ Report completion, enable export options
|
└─ Report completion, enable export options
|
||||||
↓
|
↓
|
||||||
Invoke-WinUtilISOExport (user chooses output)
|
Invoke-WinUtilISOExport (user chooses output)
|
||||||
├─ Option 1: Save as ISO
|
├─ Option 1: Save as ISO
|
||||||
│ ├─ Build bootable ISO via oscdimg.exe (BIOS/UEFI dual-boot)
|
│ ├─ Build bootable ISO via oscdimg.exe (BIOS/UEFI dual-boot)
|
||||||
│ └─ Output: Win11_Modified_[date].iso (2.5-3.5 GB)
|
│ └─ Output: Win11_Modified_[date].iso (close to the source ISO size)
|
||||||
│
|
│
|
||||||
└─ Option 2: Write to USB
|
└─ Option 2: Write to USB
|
||||||
├─ Format USB as GPT
|
├─ Format USB as GPT
|
||||||
├─ Create 512 MB EFI partition
|
├─ Create single FAT32 partition (capped at 32 GB)
|
||||||
├─ Copy modified ISO contents
|
├─ Split install.wim into .swm files if > 3.8 GB
|
||||||
|
├─ Reject install.esd files of 4 GB or more
|
||||||
|
├─ Copy files via robocopy
|
||||||
└─ Output: Bootable USB (minimum 8 GB)
|
└─ Output: Bootable USB (minimum 8 GB)
|
||||||
↓
|
↓
|
||||||
Invoke-WinUtilISOCleanAndReset (optional)
|
Invoke-WinUtilISOCleanAndReset (optional)
|
||||||
└─ Delete temp working directory (~10-15 GB)
|
├─ Dismount any open WIM mounts with discard
|
||||||
|
├─ Delete temp working directory (~10-15 GB)
|
||||||
└─ Reset UI to initial state
|
└─ Reset UI to initial state
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -211,20 +222,30 @@ Invoke-WinUtilISOCleanAndReset (optional)
|
|||||||
- Checks image metadata for "Windows 11" string
|
- Checks image metadata for "Windows 11" string
|
||||||
- Rejects custom, modified, or non-Windows 11 ISOs
|
- Rejects custom, modified, or non-Windows 11 ISOs
|
||||||
|
|
||||||
|
**WIM Metadata Validation**:
|
||||||
|
- During driver injection, `Assert-WinUtilISOWimMetadata` validates critical fields (`Languages`, `Installation`, `Edition`, `ProductSuite`, `ProductType`) before and after WIM servicing
|
||||||
|
- Driver injection stops before export if the required metadata is missing or changes
|
||||||
|
- Without driver injection, WinUtil preserves the original installation image and skips WIM metadata validation
|
||||||
|
|
||||||
|
**Edition Pinning & Setup Fallback**:
|
||||||
|
- Writes `sources\ei.cfg` and removes `sources\PID.txt` so setup does not use mismatched OEM product keys
|
||||||
|
- Pins the selected edition index in `autounattend.xml` (`/IMAGE/INDEX`)
|
||||||
|
- Stages fallback setup scripts under `sources\$OEM$\$$\Setup\Scripts\` with `UseConfigurationSet` enabled
|
||||||
|
|
||||||
**Work Session Recovery**:
|
**Work Session Recovery**:
|
||||||
- Auto-detects incomplete work from previous sessions
|
- Auto-detects incomplete work from previous sessions
|
||||||
- Allows resuming Step 4 (export) without re-running Steps 1-3
|
- Allows resuming the export step without re-running selection and modification
|
||||||
- Prevents redundant modifications
|
- Prevents redundant modifications
|
||||||
|
|
||||||
**Modification Safety**:
|
**Modification Safety**:
|
||||||
- All registry changes are documented in a script (reversible)
|
- Windows PE and specialize set hardware bypass and setup safeguards; `WinUtil-PostInstall.ps1` applies additional registry changes at first logon
|
||||||
- Original ISO never modified; only working copy
|
- Original ISO never modified; only working copy
|
||||||
- Logged to `WinUtil_Win11ISO.log` for debugging
|
- Logged to the WinUtil session log and the live UI status panel
|
||||||
- DISM handles image dismount with automatic cleanup on error
|
- DISM handles image dismount with automatic cleanup and discard on error
|
||||||
|
|
||||||
### Win11 Creator Registry Tweaks
|
### Win11 Creator Registry Tweaks
|
||||||
|
|
||||||
The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
|
The `Invoke-WinUtilISOScript` function applies **50+ registry tweaks** during setup and first logon:
|
||||||
|
|
||||||
**Hardware Bypass**:
|
**Hardware Bypass**:
|
||||||
- TPM 2.0 check bypass
|
- TPM 2.0 check bypass
|
||||||
@@ -262,9 +283,9 @@ The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
|
|||||||
|
|
||||||
### Driver Injection Feature
|
### Driver Injection Feature
|
||||||
|
|
||||||
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into both:
|
**Optional Enhancement**: When enabled, exports all drivers from the running system and injects them into:
|
||||||
- `install.wim` (main OS image)
|
- `install.wim` (main OS image), excluding stale duplicates
|
||||||
- `boot.wim` index 2 (Windows Setup PE environment)
|
- `boot.wim` index 2 (Windows Setup PE environment), storage controllers only (`SCSIAdapter` / `HDC`)
|
||||||
|
|
||||||
**Use Case**: Enables offline installation on systems with missing drivers.
|
**Use Case**: Enables offline installation on systems with missing drivers.
|
||||||
|
|
||||||
@@ -272,7 +293,7 @@ The `Invoke-WinUtilISOScript` function applies **50+ offline registry tweaks**:
|
|||||||
|
|
||||||
- **Temporary working directory**: ~10-15 GB
|
- **Temporary working directory**: ~10-15 GB
|
||||||
- **Original ISO**: 4-6 GB
|
- **Original ISO**: 4-6 GB
|
||||||
- **Modified ISO**: 2.5-3.5 GB
|
- **Modified ISO**: close to the source ISO size
|
||||||
- **Total needed**: ~25 GB for safe operation
|
- **Total needed**: ~25 GB for safe operation
|
||||||
|
|
||||||
## Data Flow
|
## Data Flow
|
||||||
@@ -389,26 +410,73 @@ Update UI
|
|||||||
- `Description`: What it does
|
- `Description`: What it does
|
||||||
- `category`: Essential/Advanced/Customize
|
- `category`: Essential/Advanced/Customize
|
||||||
- `registry`: Registry changes to make
|
- `registry`: Registry changes to make
|
||||||
|
- `registry[].Values`: Per-state values for a registry-backed combobox
|
||||||
|
- `registry[].DefaultValue`: Effective value when the registry entry is absent
|
||||||
- `service`: Services to change
|
- `service`: Services to change
|
||||||
- `OriginalValue/State`: For undo functionality
|
- `OriginalValue/State`: For undo functionality
|
||||||
|
|
||||||
## PowerShell Runspace
|
## Threading Model
|
||||||
|
|
||||||
Winutil uses PowerShell runspaces for the GUI to remain responsive:
|
WinUtil runs on three kinds of thread, and each has one job:
|
||||||
|
|
||||||
|
| Thread | Runspace | Responsibility |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Main | The one the script started in | Start the interface, wait for it, surface its errors, clean up |
|
||||||
|
| Interface | `$sync.UIRunspace`, a dedicated STA runspace | Own the window. Paint and dispatch, nothing else |
|
||||||
|
| Workers | `$sync.runspace`, a shared pool | Run everything long: installs, tweaks, features, AppX, Win11 Creator |
|
||||||
|
|
||||||
|
All three are created from the same starting point, `New-WinUtilSessionState`, which carries
|
||||||
|
`$sync`, the compiled script's globals, and every WinUtil function. That is what lets any
|
||||||
|
thread call any helper without the caller injecting function definitions.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Create runspace
|
# main.ps1 - the interface gets its own thread
|
||||||
$sync.runspace = [runspacefactory]::CreateRunspace()
|
$sync.UIRunspace = [runspacefactory]::CreateRunspace($Host, (New-WinUtilSessionState))
|
||||||
$sync.runspace.Open()
|
$sync.UIRunspace.ApartmentState = "STA"
|
||||||
$sync.runspace.SessionStateProxy.SetVariable("sync", $sync)
|
$sync.UIRunspace.Open()
|
||||||
|
|
||||||
# Run code in background
|
$uiShell = [powershell]::Create()
|
||||||
$powershell = [powershell]::Create().AddScript($scriptblock)
|
$uiShell.Runspace = $sync.UIRunspace
|
||||||
$powershell.Runspace = $sync.runspace
|
[void]$uiShell.AddScript({ Start-WinUtilUserInterface })
|
||||||
$handle = $powershell.BeginInvoke()
|
$uiHandle = $uiShell.BeginInvoke()
|
||||||
|
$uiHandle.AsyncWaitHandle.WaitOne()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why**: Prevents UI freezing during long-running operations.
|
**Why**: the window never blocks on work, and a failure on the interface thread is reported
|
||||||
|
instead of disappearing.
|
||||||
|
|
||||||
|
## Long-Running Work
|
||||||
|
|
||||||
|
Every long action goes through `Start-WinUtilJob`, which owns everything a running operation
|
||||||
|
needs: refusing to start while another job runs, the busy flag (`$sync.ActiveJob`), the progress
|
||||||
|
bar and taskbar item, the boxed console banner, a start/finish/failure line in the log, and
|
||||||
|
restoring the interface in a `finally` whatever happens.
|
||||||
|
|
||||||
|
`Invoke-WPFButton` decides what counts as a long action. Anything that changes the system gets a
|
||||||
|
job; anything that only changes what the interface is showing runs on the interface thread.
|
||||||
|
That single classification is why no workflow arranges its own progress, banner or busy state.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Start-WinUtilJob -Name "Features" -Description "Installing Windows Features" -Parameters @{
|
||||||
|
Features = @($sync.selectedFeatures)
|
||||||
|
} -ScriptBlock {
|
||||||
|
param($Features)
|
||||||
|
|
||||||
|
$total = @($Features).Count
|
||||||
|
$completed = 0
|
||||||
|
foreach ($feature in $Features) {
|
||||||
|
$completed++
|
||||||
|
Step-WinUtilJob -Status "Installing $feature ($completed/$total)" -Percent ([int](($completed / $total) * 100))
|
||||||
|
Invoke-WinUtilFeatureInstall $feature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The body only has to do the work and call `Step-WinUtilJob`. Anything it throws is
|
||||||
|
caught, logged, and shown on the taskbar as a failure.
|
||||||
|
|
||||||
|
**Values, not closures**: the body is rebuilt inside the worker from its text, so it receives
|
||||||
|
what it needs through `-Parameters` rather than capturing the caller's variables.
|
||||||
|
|
||||||
## WPF Event Handling
|
## WPF Event Handling
|
||||||
|
|
||||||
@@ -454,23 +522,21 @@ if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
|
|||||||
choco install $app.choco -y
|
choco install $app.choco -y
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling And Logging
|
||||||
|
|
||||||
Winutil uses PowerShell error handling:
|
A job body does not need its own error handling. `Start-WinUtilJob` catches whatever the body
|
||||||
|
throws, logs it, and marks the run as failed on the taskbar, so a failure can never leave the
|
||||||
|
interface stuck busy. Only catch inside a body when you have something specific to do first,
|
||||||
|
such as cleaning up a mounted image, and then rethrow.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
try {
|
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "winget install failed: $($_.Exception.Message)"
|
||||||
# Attempt operation
|
|
||||||
Invoke-SomeOperation
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "Error: $_" -ForegroundColor Red
|
|
||||||
# Log error
|
|
||||||
Add-Content -Path $logfile -Value "ERROR: $_"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Logging**: Errors and operations are logged for debugging.
|
Entries and console diagnostics go to the same
|
||||||
|
`%LocalAppData%\winutil\logs\winutil_<timestamp>.log` session file. While `Start-Transcript`
|
||||||
|
owns that file, `Write-WinUtilLog` writes through the host so the transcript captures the entry
|
||||||
|
without a competing direct file write.
|
||||||
|
|
||||||
## Configuration Loading
|
## Configuration Loading
|
||||||
|
|
||||||
@@ -488,15 +554,23 @@ $sync.configs.features = Get-Content "config/feature.json" | ConvertFrom-Json
|
|||||||
|
|
||||||
## UI Update Pattern
|
## UI Update Pattern
|
||||||
|
|
||||||
UI updates must happen on the UI thread:
|
Controls may only be touched from the thread that owns the window, so background work reaches
|
||||||
|
them through `Invoke-WPFUIThread`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$sync.form.Dispatcher.Invoke([action]{
|
Invoke-WPFUIThread -Parameters @{ Count = $installed.Count } -ScriptBlock {
|
||||||
$sync.WPFStatusLabel.Content = "Installing..."
|
param($Count)
|
||||||
}, "Normal")
|
$sync.WPFselectedAppsButton.Content = "Selected Apps: $Count"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why**: WPF requires UI updates on the main thread.
|
Add `-Async` to post the update instead of waiting for it. `Step-WinUtilJob` and the
|
||||||
|
Win11 Creator status log use that so a per-item update never stalls the worker.
|
||||||
|
|
||||||
|
**Values, not closures**: the body is rebuilt inside the interface runspace, so it takes what it
|
||||||
|
needs through `-Parameters`. Handing over a scriptblock from a worker instead would keep that
|
||||||
|
worker's session state, which loses the caller's variables on an async post and costs roughly
|
||||||
|
twenty times as much per command - enough to turn a checkbox refresh into a visible freeze.
|
||||||
|
|
||||||
## Adding New Features
|
## Adding New Features
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ This page is generated from [`functions/public/Invoke-WPFPanelAutologin.ps1`](ht
|
|||||||
|
|
||||||
```powershell title="functions/public/Invoke-WPFPanelAutologin.ps1"
|
```powershell title="functions/public/Invoke-WPFPanelAutologin.ps1"
|
||||||
function Invoke-WPFPanelAutologin {
|
function Invoke-WPFPanelAutologin {
|
||||||
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile "$winutildir\autologin.exe"
|
$autologonPath = Join-Path $sync.winutildir "autologin.exe"
|
||||||
Start-Process -FilePath "$winutildir\autologin.exe" -ArgumentList /accepteula
|
Invoke-WebRequest -Uri https://live.sysinternals.com/Autologon.exe -OutFile $autologonPath
|
||||||
|
Start-Process -FilePath $autologonPath -ArgumentList /accepteula
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,11 +21,54 @@ function Invoke-WPFSystemRepair {
|
|||||||
3. DISM - Repair a corrupted Windows operating system image
|
3. DISM - Repair a corrupted Windows operating system image
|
||||||
#>
|
#>
|
||||||
|
|
||||||
Start-Process cmd.exe -ArgumentList "/c chkdsk /scan /perf" -NoNewWindow -Wait
|
# SuccessCodes maps the non-zero exits a step treats as success to what they mean. The codes
|
||||||
Start-Process cmd.exe -ArgumentList "/c sfc /scannow" -NoNewWindow -Wait
|
# are per step because the same number means different things: 1 and 2 are ordinary chkdsk
|
||||||
Start-Process cmd.exe -ArgumentList "/c dism /online /cleanup-image /restorehealth" -NoNewWindow -Wait
|
# outcomes, while 1 from sfc is a failure, and 3010 is a repaired image from DISM only.
|
||||||
|
$steps = @(
|
||||||
|
@{
|
||||||
|
Label = "Checking the disk for errors"
|
||||||
|
Arguments = "/c chkdsk /scan /perf"
|
||||||
|
# 3 is left out: the disk could not be checked, or has errors an online scan cannot
|
||||||
|
# fix, and the steps after this one are not worth running on a disk in that state.
|
||||||
|
SuccessCodes = @{
|
||||||
|
1 = "errors were found and fixed"
|
||||||
|
2 = "cleanup was performed, or was skipped because /f was not given"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Label = "Scanning protected system files"
|
||||||
|
Arguments = "/c sfc /scannow"
|
||||||
|
SuccessCodes = @{}
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Label = "Repairing the Windows image"
|
||||||
|
Arguments = "/c dism /online /cleanup-image /restorehealth"
|
||||||
|
SuccessCodes = @{
|
||||||
|
3010 = "a restart is needed for the repair to take effect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Write-Host "==> Finished System Repair"
|
$completed = 0
|
||||||
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
|
foreach ($step in $steps) {
|
||||||
|
Step-WinUtilJob -Status "$($step.Label) ($($completed + 1)/$($steps.Count))" -Percent ([int](($completed / $steps.Count) * 100))
|
||||||
|
Write-WinUtilLog -Component "SystemRepair" -Message $step.Label
|
||||||
|
# Start-Process does not throw on a nonzero exit, so without this a failed chkdsk, sfc
|
||||||
|
# or dism run would still be reported as a completed repair
|
||||||
|
$process = Start-Process cmd.exe -ArgumentList $step.Arguments -NoNewWindow -Wait -PassThru
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
if ($step.SuccessCodes.ContainsKey($exitCode)) {
|
||||||
|
# Start-WinUtilJob records WarningRecord output in both the session log and the
|
||||||
|
# job result, so accepted nonzero outcomes cannot finish with a green checkmark.
|
||||||
|
Write-Warning "$($step.Label) finished: $($step.SuccessCodes[$exitCode])."
|
||||||
|
} else {
|
||||||
|
throw "$($step.Label) failed with exit code $exitCode."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$completed++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,8 +25,5 @@ function Invoke-WPFFixesNTPPool {
|
|||||||
Restart-Service w32time
|
Restart-Service w32time
|
||||||
w32tm /resync
|
w32tm /resync
|
||||||
|
|
||||||
Write-Host "================================="
|
|
||||||
Write-Host "-- NTP Configuration Complete ---"
|
|
||||||
Write-Host "================================="
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function Invoke-WPFFixesUpdate {
|
|||||||
param($Aggressive = $false)
|
param($Aggressive = $false)
|
||||||
|
|
||||||
Write-Progress -Id 0 -Activity "Repairing Windows Update" -PercentComplete 0
|
Write-Progress -Id 0 -Activity "Repairing Windows Update" -PercentComplete 0
|
||||||
Set-WinUtilTaskbaritem -state "Indeterminate" -overlay "logo"
|
Step-WinUtilJob -State "Indeterminate"
|
||||||
Write-Host "Starting Windows Update Repair..."
|
Write-Host "Starting Windows Update Repair..."
|
||||||
# Wait for the first progress bar to show, otherwise the second one won't show
|
# Wait for the first progress bar to show, otherwise the second one won't show
|
||||||
Start-Sleep -Milliseconds 200
|
Start-Sleep -Milliseconds 200
|
||||||
@@ -125,9 +125,9 @@ function Invoke-WPFFixesUpdate {
|
|||||||
if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate") {
|
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 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
|
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"
|
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "AccountDomainSid" -ErrorAction SilentlyContinue
|
||||||
Start-Process -NoNewWindow -FilePath "REG" -ArgumentList "DELETE", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate", "/v", "PingID", "/f" -RedirectStandardError "NUL"
|
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate" -Name "PingID" -ErrorAction SilentlyContinue
|
||||||
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 "SusClientId" -ErrorAction SilentlyContinue
|
||||||
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -Status "Completed" -PercentComplete 100
|
Write-Progress -Id 6 -ParentId 0 -Activity "Removing WSUS client settings" -Status "Completed" -PercentComplete 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,24 +203,14 @@ function Invoke-WPFFixesUpdate {
|
|||||||
try {
|
try {
|
||||||
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
|
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
|
||||||
} catch {
|
} catch {
|
||||||
Set-WinUtilTaskbaritem -state "Error" -overlay "warning"
|
Write-WinUtilLog -Level "ERROR" -Component "Updates" -Message "Failed to create Windows Update COM object: $_"
|
||||||
Write-Warning "Failed to create Windows Update COM object: $_"
|
Write-Warning "Failed to create Windows Update COM object: $_"
|
||||||
}
|
}
|
||||||
Start-Process -NoNewWindow -FilePath "wuauclt" -ArgumentList "/resetauthorization", "/detectnow"
|
Start-Process -NoNewWindow -FilePath "wuauclt" -ArgumentList "/resetauthorization", "/detectnow"
|
||||||
Write-Progress -Id 10 -ParentId 0 -Activity "Forcing discovery" -Status "Completed" -PercentComplete 100
|
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
|
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Completed" -PercentComplete 100
|
||||||
|
|
||||||
Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"
|
Show-WinUtilMessage -Message "Stock settings loaded.`n Please reboot your computer" -Title "Reset Windows Update" -Button "OK" -Icon "Information" | Out-Null
|
||||||
|
|
||||||
$ButtonType = [System.Windows.MessageBoxButton]::OK
|
|
||||||
$MessageboxTitle = "Reset Windows Update "
|
|
||||||
$Messageboxbody = ("Stock settings loaded.`n Please reboot your computer")
|
|
||||||
$MessageIcon = [System.Windows.MessageBoxImage]::Information
|
|
||||||
|
|
||||||
[System.Windows.MessageBox]::Show($Messageboxbody, $MessageboxTitle, $ButtonType, $MessageIcon)
|
|
||||||
Write-Host "==============================================="
|
|
||||||
Write-Host "-- Reset All Windows Update Settings to Stock -"
|
|
||||||
Write-Host "==============================================="
|
|
||||||
|
|
||||||
# Remove the progress bars
|
# Remove the progress bars
|
||||||
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed
|
Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed
|
||||||
|
|||||||
@@ -18,18 +18,8 @@ function Invoke-WPFFixesWinget {
|
|||||||
.DESCRIPTION
|
.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
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Step-WinUtilJob -Status "Repairing WinGet" -State "Indeterminate"
|
||||||
|
Install-WinUtilWinget -Force
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,19 +9,72 @@ This page is generated from [`functions/private/Invoke-WinUtilInstallPSProfile.p
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
```powershell title="functions/private/Invoke-WinUtilInstallPSProfile.ps1"
|
```powershell title="functions/private/Invoke-WinUtilInstallPSProfile.ps1"
|
||||||
|
function Get-WinUtilPowerShell7Path {
|
||||||
|
$command = Get-Command pwsh -CommandType Application -ErrorAction SilentlyContinue
|
||||||
|
if ($command) { return $command.Source }
|
||||||
|
|
||||||
|
foreach ($candidate in @(
|
||||||
|
"$env:ProgramFiles\PowerShell\7\pwsh.exe",
|
||||||
|
"$env:LOCALAPPDATA\Microsoft\WindowsApps\pwsh.exe")) {
|
||||||
|
if (Test-Path -LiteralPath $candidate) { return $candidate }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-WinUtilInstallPSProfile {
|
function Invoke-WinUtilInstallPSProfile {
|
||||||
if (-not (Get-Command wt)) {
|
<#
|
||||||
Write-Host "Windows Terminal not found. Installing..."
|
.SYNOPSIS
|
||||||
|
Installs the CTT PowerShell profile
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
The profile targets PowerShell 7, so its setup script has to run under pwsh rather than
|
||||||
|
the runspace this job is on. It runs as a child process with its output captured, so the
|
||||||
|
job log records what happened instead of it scrolling past in a terminal nobody kept.
|
||||||
|
#>
|
||||||
|
|
||||||
|
$pwshPath = Get-WinUtilPowerShell7Path
|
||||||
|
if (-not $pwshPath) {
|
||||||
|
Step-WinUtilJob -Status "Installing PowerShell 7" -State "Indeterminate"
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "PowerShell 7 not found, installing it first."
|
||||||
|
|
||||||
Install-WinUtilWinget
|
Install-WinUtilWinget
|
||||||
winget install Microsoft.WindowsTerminal --source winget --silent
|
Install-WinUtilProgramWinget -Action Install -Programs @("Microsoft.PowerShell") | Out-Null
|
||||||
|
|
||||||
|
# WinGet updates the persisted PATH, not this already-running process. Resolve the
|
||||||
|
# standard install locations as well as the current PATH before deciding it failed.
|
||||||
|
$pwshPath = Get-WinUtilPowerShell7Path
|
||||||
|
if (-not $pwshPath) {
|
||||||
|
throw "PowerShell 7 could not be installed, so the profile cannot be set up."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Get-Command pwsh)) {
|
Step-WinUtilJob -Status "Running the profile setup" -State "Indeterminate"
|
||||||
Write-Host "PowerShell 7 not found. Installing..."
|
|
||||||
Install-WinUtilWinget
|
$setupUrl = "https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1"
|
||||||
winget install Microsoft.PowerShell --source winget --installer-type wix --silent
|
# Stop in the child, so a setup failure is a nonzero exit rather than a logged error and a
|
||||||
|
# exit code of zero
|
||||||
|
$output = & $pwshPath -NoProfile -NonInteractive -Command "`$ErrorActionPreference = 'Stop'; irm '$setupUrl' | iex" 2>&1
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
$failures = 0
|
||||||
|
foreach ($line in @($output)) {
|
||||||
|
if ($line -is [System.Management.Automation.ErrorRecord]) {
|
||||||
|
$failures++
|
||||||
|
Write-WinUtilErrorRecord -ErrorRecord $line -Component "Feature" -Context "PowerShell profile setup"
|
||||||
|
} elseif (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message ([string]$line).Trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wt new-tab pwsh -NoExit -Command "irm https://github.com/ChrisTitusTech/powershell-profile/raw/main/setup.ps1 | iex"
|
if ($exitCode -ne 0) {
|
||||||
|
throw "The profile setup script exited with code $exitCode."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failures -gt 0) {
|
||||||
|
throw "The profile setup script reported $failures error(s); see the log."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "CTT PowerShell profile installed. Open a new PowerShell 7 session to use it."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -10,13 +10,39 @@ This page is generated from [`functions/private/Invoke-WinUtilUninstallPSProfile
|
|||||||
|
|
||||||
```powershell title="functions/private/Invoke-WinUtilUninstallPSProfile.ps1"
|
```powershell title="functions/private/Invoke-WinUtilUninstallPSProfile.ps1"
|
||||||
function Invoke-WinUtilUninstallPSProfile {
|
function Invoke-WinUtilUninstallPSProfile {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Restores the PowerShell 7 profile the CTT profile replaced
|
||||||
|
|
||||||
if (Test-Path ($Profile + ".bak")) {
|
.DESCRIPTION
|
||||||
Move-Item -Path ($Profile + ".bak") -Destination $Profile
|
The profile path has to come from pwsh itself. $PROFILE inside this job is the worker's
|
||||||
} else {
|
own Windows PowerShell profile, which is not the file the install wrote.
|
||||||
Remove-Item -Path $Profile
|
#>
|
||||||
|
|
||||||
|
$pwshPath = Get-WinUtilPowerShell7Path
|
||||||
|
if (-not $pwshPath) {
|
||||||
|
throw "PowerShell 7 is not installed, so there is no CTT profile to remove."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Successfully uninstalled CTT PowerShell Profile." -ForegroundColor Green
|
$profilePath = (& $pwshPath -NoProfile -NonInteractive -Command '$PROFILE' | Select-Object -First 1)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($profilePath)) {
|
||||||
|
throw "Could not determine the PowerShell 7 profile path."
|
||||||
|
}
|
||||||
|
$profilePath = $profilePath.Trim()
|
||||||
|
$backupPath = "$profilePath.bak"
|
||||||
|
|
||||||
|
if (Test-Path $backupPath) {
|
||||||
|
Move-Item -Path $backupPath -Destination $profilePath -Force
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Restored the profile that was in place before: $profilePath"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $profilePath) {
|
||||||
|
Remove-Item -Path $profilePath -Force
|
||||||
|
Write-WinUtilLog -Component "Feature" -Message "Removed the CTT PowerShell profile: $profilePath"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "Feature" -Message "No PowerShell 7 profile found at $profilePath, nothing to remove."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,17 +13,10 @@ function Invoke-WPFSSHServer {
|
|||||||
<#
|
<#
|
||||||
|
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Invokes the OpenSSH Server install in a runspace
|
Installs and starts the OpenSSH Server
|
||||||
|
|
||||||
#>
|
#>
|
||||||
|
|
||||||
Invoke-WPFRunspace -ScriptBlock {
|
Invoke-WinUtilSSHServer
|
||||||
|
|
||||||
Invoke-WinUtilSSHServer
|
|
||||||
|
|
||||||
Write-Host "======================================="
|
|
||||||
Write-Host "-- OpenSSH Server installed! ---"
|
|
||||||
Write-Host "======================================="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -9,7 +9,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
```json title="config/tweaks.json"
|
```json title="config/tweaks.json"
|
||||||
"WPFTweaksDisableLockscreen": {
|
"WPFToggleDisableLockscreen": {
|
||||||
"Content": "Lock Screen - Disable",
|
"Content": "Lock Screen - Disable",
|
||||||
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
|
"Description": "Skips the lock screen entirely and goes directly to the sign-in screen on boot and wake.",
|
||||||
"category": "Customize Preferences",
|
"category": "Customize Preferences",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Multiplane Overlay"
|
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
|
editUrl: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,28 +9,40 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
```json title="config/tweaks.json"
|
```json title="config/tweaks.json"
|
||||||
"WPFToggleMultiplaneOverlay": {
|
"WPFMultiplaneOverlay": {
|
||||||
"Content": "Multiplane Overlay",
|
"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",
|
"category": "Customize Preferences",
|
||||||
"panel": "2",
|
"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": [
|
"registry": [
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\Dwm",
|
"Path": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\Dwm",
|
||||||
"Name": "OverlayTestMode",
|
"Name": "OverlayTestMode",
|
||||||
"Value": "0",
|
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "5",
|
"DefaultValue": "0",
|
||||||
"DefaultState": "true"
|
"Values": {
|
||||||
|
"Enabled": "<RemoveEntry>",
|
||||||
|
"Disabled (Compatibility)": "5",
|
||||||
|
"Fully Disabled": "5"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers",
|
"Path": "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers",
|
||||||
"Name": "DisableOverlays",
|
"Name": "DisableOverlays",
|
||||||
"Value": "0",
|
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "1",
|
"DefaultValue": "0",
|
||||||
"DefaultState": "true"
|
"Values": {
|
||||||
|
"Enabled": "<RemoveEntry>",
|
||||||
|
"Disabled (Compatibility)": "<RemoveEntry>",
|
||||||
|
"Fully Disabled": "1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Microsoft Outlook New Version"
|
title: "Microsoft Outlook New Version"
|
||||||
description: "This will ensures the classic Outlook application is used."
|
description: "This will ensure the new Outlook application is used."
|
||||||
editUrl: false
|
editUrl: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
```json title="config/tweaks.json"
|
```json title="config/tweaks.json"
|
||||||
"WPFToggleNewOutlook": {
|
"WPFToggleNewOutlook": {
|
||||||
"Content": "Microsoft Outlook New Version",
|
"Content": "Microsoft Outlook New Version",
|
||||||
"Description": "This will ensures the classic Outlook application is used.",
|
"Description": "This will ensure the new Outlook application is used.",
|
||||||
"category": "Customize Preferences",
|
"category": "Customize Preferences",
|
||||||
"panel": "2",
|
"panel": "2",
|
||||||
"Type": "Toggle",
|
"Type": "Toggle",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Activity History - Disable"
|
title: "Activity History - Disable"
|
||||||
description: "Erases recent docs, clipboard, and run history."
|
description: "Stops Windows from publishing or uploading user activities while preserving clipboard history."
|
||||||
editUrl: false
|
editUrl: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,14 +11,14 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
```json title="config/tweaks.json"
|
```json title="config/tweaks.json"
|
||||||
"WPFTweaksActivity": {
|
"WPFTweaksActivity": {
|
||||||
"Content": "Activity History - Disable",
|
"Content": "Activity History - Disable",
|
||||||
"Description": "Erases recent docs, clipboard, and run history.",
|
"Description": "Stops Windows from publishing or uploading user activities while preserving clipboard history.",
|
||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"registry": [
|
"registry": [
|
||||||
{
|
{
|
||||||
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
|
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\System",
|
||||||
"Name": "EnableActivityFeed",
|
"Name": "EnableActivityFeed",
|
||||||
"Value": "0",
|
"Value": "1",
|
||||||
"Type": "DWord",
|
"Type": "DWord",
|
||||||
"OriginalValue": "<RemoveEntry>"
|
"OriginalValue": "<RemoveEntry>"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
"panel": "1",
|
"panel": "1",
|
||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"
|
"
|
||||||
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force
|
# A temp folder always holds files something has open, including this run's own, and
|
||||||
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force
|
# the job layer counts a logged error as a failed step
|
||||||
|
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny Everyone:F"
|
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /deny *S-1-1-0:F"
|
||||||
],
|
],
|
||||||
"UndoScript": [
|
"UndoScript": [
|
||||||
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant Everyone:F"
|
"icacls \"$Env:LocalAppData\\Packages\\Microsoft.WindowsStore_8wekyb3d8bbwe\\LocalState\\store.db\" /grant *S-1-1-0:F"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "End Task With Right Click - Enable"
|
title: "End Task With Right Click - Enable"
|
||||||
description: "Enables option to end task when right clicking a program in the taskbar."
|
description: "Enables option to end task when right-clicking a program in the taskbar."
|
||||||
editUrl: false
|
editUrl: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
```json title="config/tweaks.json"
|
```json title="config/tweaks.json"
|
||||||
"WPFTweaksEndTaskOnTaskbar": {
|
"WPFTweaksEndTaskOnTaskbar": {
|
||||||
"Content": "End Task With Right Click - Enable",
|
"Content": "End Task With Right Click - Enable",
|
||||||
"Description": "Enables option to end task when right clicking a program in the taskbar.",
|
"Description": "Enables option to end task when right-clicking a program in the taskbar.",
|
||||||
"category": "Essential Tweaks",
|
"category": "Essential Tweaks",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"registry": [
|
"registry": [
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
"service": [
|
"service": [
|
||||||
{
|
{
|
||||||
"Name": "lfsvc",
|
"Name": "lfsvc",
|
||||||
"StartupType": "Disable",
|
"StartupType": "Disabled",
|
||||||
"OriginalType": "Manual"
|
"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).
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "Logitech Download Assistant Auto-Install - Disable"
|
||||||
|
description: "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it."
|
||||||
|
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"
|
||||||
|
"WPFTweaksLogiBlock": {
|
||||||
|
"Content": "Logitech Download Assistant Auto-Install - Disable",
|
||||||
|
"Description": "Blocks the Logi Download Assistant that Windows Update keeps reinstalling with Logitech device drivers. Logitech hardware keeps working without it.",
|
||||||
|
"category": "z__Advanced Tweaks - CAUTION",
|
||||||
|
"panel": "1",
|
||||||
|
"InvokeScript": [
|
||||||
|
"
|
||||||
|
Stop-Process -Name \"logi_download_assistant\" -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||||
|
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||||
|
|
||||||
|
if (Test-Path $LogiPath) {
|
||||||
|
Remove-Item $LogiPath\\* -Recurse -Force
|
||||||
|
} else {
|
||||||
|
New-Item -Path $LogiPath -ItemType Directory
|
||||||
|
}
|
||||||
|
|
||||||
|
icacls $LogiPath /deny \"*S-1-1-0:(W)\"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to deny write access on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||||
|
"
|
||||||
|
],
|
||||||
|
"UndoScript": [
|
||||||
|
"
|
||||||
|
$ProgramFiles64 = if ($Env:ProgramW6432) { $Env:ProgramW6432 } else { $Env:ProgramFiles }
|
||||||
|
$LogiPath = \"$ProgramFiles64\\LogiDownloadAssistant\"
|
||||||
|
|
||||||
|
if (Test-Path $LogiPath) {
|
||||||
|
icacls $LogiPath /remove:d \"*S-1-1-0\"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw \"icacls failed to remove the write-deny rule on $LogiPath (exit code $LASTEXITCODE)\" }
|
||||||
|
}
|
||||||
|
"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -10,53 +10,21 @@ This page is generated from [`functions/public/Invoke-WPFOOSU.ps1`](https://gith
|
|||||||
|
|
||||||
```powershell title="functions/public/Invoke-WPFOOSU.ps1"
|
```powershell title="functions/public/Invoke-WPFOOSU.ps1"
|
||||||
function Invoke-WPFOOSU {
|
function Invoke-WPFOOSU {
|
||||||
if ($sync.ProcessRunning) {
|
Start-WinUtilJob -Name "OOSU" -Description "Downloading O&O ShutUp10++" -Parameters @{
|
||||||
Show-WinUtilMessage -Message "Another process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
|
DownloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
|
||||||
return
|
} -ScriptBlock {
|
||||||
}
|
param($DownloadPath)
|
||||||
|
|
||||||
$downloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
|
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
|
||||||
$sync.ProcessRunning = $true
|
|
||||||
|
|
||||||
Invoke-WPFRunspace -ParameterList @(,("downloadPath", $downloadPath)) -ScriptBlock {
|
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $DownloadPath -ProgressCallback {
|
||||||
param($downloadPath)
|
param($percent)
|
||||||
|
Step-WinUtilJob -Status "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
|
||||||
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
|
|
||||||
if ($hasUI) {
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ (0%)" -Percent 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $downloadPath -ProgressCallback {
|
|
||||||
param($percent)
|
|
||||||
|
|
||||||
if ($hasUI) {
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasUI) {
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Launching O&O ShutUp10++" -Percent 100
|
|
||||||
}
|
|
||||||
Start-Process -FilePath $downloadPath
|
|
||||||
|
|
||||||
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
|
|
||||||
if ($hasUI) {
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ launched" -Percent 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-WinUtilLog -Level "ERROR" -Component "OOSU" -Message "O&O ShutUp10++ download failed: $($_.Exception.Message)"
|
|
||||||
if ($hasUI) {
|
|
||||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ download failed" -Percent 100
|
|
||||||
}
|
|
||||||
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$sync.ProcessRunning = $false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Step-WinUtilJob -Status "Launching O&O ShutUp10++" -Percent 100
|
||||||
|
Start-Process -FilePath $DownloadPath
|
||||||
|
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
New-Item -Path $RazerPath -ItemType Directory
|
New-Item -Path $RazerPath -ItemType Directory
|
||||||
}
|
}
|
||||||
|
|
||||||
icacls $RazerPath /deny \"Everyone:(W)\"
|
icacls $RazerPath /deny \"*S-1-1-0:(W)\"
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
"UndoScript": [
|
"UndoScript": [
|
||||||
"
|
"
|
||||||
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d Everyone
|
icacls \"$Env:SystemRoot\\Installer\\Razer\" /remove:d *S-1-1-0
|
||||||
"
|
"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
"InvokeScript": [
|
"InvokeScript": [
|
||||||
"
|
"
|
||||||
# Deny permission to remove OneDrive folder
|
# Deny permission to remove OneDrive folder
|
||||||
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
|
icacls $Env:OneDrive /deny \"*S-1-5-32-544:(D,DC)\"
|
||||||
|
|
||||||
Write-Host \"Uninstalling OneDrive...\"
|
Write-Host \"Uninstalling OneDrive...\"
|
||||||
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
|
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
|
||||||
@@ -31,7 +31,7 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
|
Remove-Item \"$Env:ProgramData\\Microsoft OneDrive\" -Recurse -Force
|
||||||
|
|
||||||
# Grant back permission to access OneDrive folder
|
# Grant back permission to access OneDrive folder
|
||||||
icacls $Env:OneDrive /grant \"Administrators:(D,DC)\"
|
icacls $Env:OneDrive /grant \"*S-1-5-32-544:(D,DC)\"
|
||||||
|
|
||||||
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
|
if (-not (Get-ChildItem -Path $Env:OneDrive)) {
|
||||||
Remove-Item -Path $Env:OneDrive -Recurse
|
Remove-Item -Path $Env:OneDrive -Recurse
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ This page is generated from [`config/tweaks.json`](https://github.com/ChrisTitus
|
|||||||
"category": "z__Advanced Tweaks - CAUTION",
|
"category": "z__Advanced Tweaks - CAUTION",
|
||||||
"panel": "1",
|
"panel": "1",
|
||||||
"Type": "Combobox",
|
"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 Fastest Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ While you can make your changes directly through the Web, we recommend cloning t
|
|||||||
* Run the following command to compile and run WinUtil:
|
* Run the following command to compile and run WinUtil:
|
||||||
* `.\Compile.ps1 -run`
|
* `.\Compile.ps1 -run`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
|
* After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ Yes, WinUtil works on Windows Server editions, though some features may not be a
|
|||||||
|
|
||||||
### Does WinUtil work with Windows LTSC?
|
### Does WinUtil work with Windows LTSC?
|
||||||
|
|
||||||
Yes, WinUtil works with Windows 10/11 LTSC editions. Some applications may not be available depending on your configuration.
|
WinUtil currently targets Windows 11. Windows 10 LTSC is not supported, consistent with the Windows 10 support policy above. Some features and applications may vary by Windows 11 edition.
|
||||||
|
|
||||||
### Can I use WinUtil in a corporate/enterprise environment?
|
### Can I use WinUtil in a corporate/enterprise environment?
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ Use the Applications tab to install, upgrade, uninstall, and review supported ap
|
|||||||
|
|
||||||

|

|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
<TabItem label="Category Filters">
|
||||||
|
* Click a category chip at the top of the tab to show only that category. The chip stays highlighted while its filter is active.
|
||||||
|
* Hold `Ctrl` and click to add more categories to the filter, or to remove one again.
|
||||||
|
* Click `All`, or click the highlighted category again while it is the only one selected, to clear the filter.
|
||||||
|
* Categories with matching results open while a filter is active. Ones that filtering opened for you go back to collapsed when you clear it, ones you opened yourself stay open.
|
||||||
|
</TabItem>
|
||||||
<TabItem label="Selected Apps Counter">
|
<TabItem label="Selected Apps Counter">
|
||||||
* The `Selected Apps` counter in the sidebar shows how many applications are currently selected.
|
* The `Selected Apps` counter in the sidebar shows how many applications are currently selected.
|
||||||
* Use it to keep track of your selection as you browse categories.
|
* Use it to keep track of your selection as you browse categories.
|
||||||
@@ -57,7 +63,7 @@ Use the Applications tab to install, upgrade, uninstall, and review supported ap
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
If you have trouble finding an application, press `Ctrl + F` and search for its name. The list filters as you type.
|
If you have trouble finding an application, press `Ctrl + F` and search for its name. The list filters as you type. The search and the category chips work together, so you can search inside the categories you picked.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ Example:
|
|||||||
To view exactly what each preset does, see:
|
To view exactly what each preset does, see:
|
||||||
https://github.com/ChrisTitusTech/winutil/blob/main/config/preset.json
|
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:
|
To create your own config file:
|
||||||
|
|
||||||
1. Open WinUtil.
|
1. Open WinUtil.
|
||||||
@@ -40,6 +42,14 @@ This is useful for:
|
|||||||
- Reusing a known-good baseline after reinstalling Windows
|
- Reusing a known-good baseline after reinstalling Windows
|
||||||
- Standardizing deployments for labs, workstations, or personal setups
|
- 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
|
:::note
|
||||||
Run the command in an elevated PowerShell session so WinUtil can apply system-level changes.
|
Run the command in an elevated PowerShell session so WinUtil can apply system-level changes.
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -56,3 +56,7 @@ Open old-school Windows panels directly from WinUtil. Available panels include:
|
|||||||
Enable an OpenSSH server on your Windows machine for remote access.
|
Enable an OpenSSH server on your Windows machine for remote access.
|
||||||
|
|
||||||
Only enable this if you intend to use remote shell access. After turning it on, verify your firewall rules and account permissions before exposing the machine to other devices.
|
Only enable this if you intend to use remote shell access. After turning it on, verify your firewall rules and account permissions before exposing the machine to other devices.
|
||||||
|
|
||||||
|
Because WinUtil runs elevated, the account it sets up is an administrator, and sshd reads administrator keys from `C:\ProgramData\ssh\administrators_authorized_keys` rather than from your profile. WinUtil creates that file and restricts it to Administrators and SYSTEM, which is what sshd requires. Add your public keys there. If an earlier WinUtil version changed `sshd_config` to read administrator keys from `%USERPROFILE%\.ssh\authorized_keys`, that is undone and any keys in it are copied across, so key auth keeps working.
|
||||||
|
|
||||||
|
Non-administrator accounts keep using `%USERPROFILE%\.ssh\authorized_keys` and need no extra setup.
|
||||||
|
|||||||
@@ -235,6 +235,21 @@ Now that you're set up, explore these guides:
|
|||||||
- [Tweaks Guide](/guides/tweaks/) — Understand system optimizations
|
- [Tweaks Guide](/guides/tweaks/) — Understand system optimizations
|
||||||
- [FAQ](/faq/) — Common questions and answers
|
- [FAQ](/faq/) — Common questions and answers
|
||||||
|
|
||||||
|
## Exporting a diagnostics report
|
||||||
|
|
||||||
|
If you're reporting a problem, click the gear icon in the top-right corner and choose **Export Environment Report**. It saves a read-only JSON file with:
|
||||||
|
|
||||||
|
- Windows edition, version, build, and architecture
|
||||||
|
- CPU model, logical processor count, and total memory
|
||||||
|
- PowerShell edition, version, and execution policy
|
||||||
|
- Whether WinGet and Chocolatey are installed, and their versions
|
||||||
|
- Whether a reboot is pending
|
||||||
|
- The current applied/not-applied state of every tweak and toggle
|
||||||
|
|
||||||
|
It does not include computer or user names, paths, IP or MAC addresses, serial numbers, installed-app inventories, services, raw registry paths or values, secrets, or logs. The tweak/toggle state is derived from a registry comparison, but only the resulting true/false per tweak is included, not any registry content itself. WinUtil never uploads the report — you choose where to save it and who to send it to.
|
||||||
|
|
||||||
|
You'll be asked whether to also bundle the last 7 days of WinUtil logs into a companion `.txt` file, which maintainers often need alongside the report to diagnose an issue. This companion is raw log output and does not use the JSON report's privacy allowlist: it may contain local paths, commands, and error details. Review it before sharing.
|
||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
If you need assistance:
|
If you need assistance:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Use the quick-selection buttons at the top of the Tweaks tab to speed up setup:
|
|||||||
* **Minimal**: Selects a smaller, lower-impact set of common tweaks.
|
* **Minimal**: Selects a smaller, lower-impact set of common tweaks.
|
||||||
* **Advanced**: Selects a focused set of safer advanced tweaks. This preset intentionally skips restore point creation and cleanup tasks to avoid a long runtime.
|
* **Advanced**: Selects a focused set of safer advanced tweaks. This preset intentionally skips restore point creation and cleanup tasks to avoid a long runtime.
|
||||||
* **Clear**: Clears all currently selected tweaks.
|
* **Clear**: Clears all currently selected tweaks.
|
||||||
* **Get Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
|
* **Select Installed Tweaks**: Best-effort detection for tweaks already applied to your system.
|
||||||
|
|
||||||
### Run Tweaks
|
### Run Tweaks
|
||||||
* **Open the Tweaks tab**: Navigate to the **Tweaks** tab in the application.
|
* **Open the Tweaks tab**: Navigate to the **Tweaks** tab in the application.
|
||||||
@@ -65,6 +65,7 @@ Use the DNS section to switch both IPv4 and IPv6 DNS providers without editing a
|
|||||||
|
|
||||||
* **Default**: Uses the default DNS settings configured by your ISP or network.
|
* **Default**: Uses the default DNS settings configured by your ISP or network.
|
||||||
* **DHCP**: Automatically acquires DNS settings from the DHCP server.
|
* **DHCP**: Automatically acquires DNS settings from the DHCP server.
|
||||||
|
* **Fastest**: Compares TCP port 53 connection times to Google and Cloudflare, then applies the quicker provider. Filtering providers are excluded. If both probes fail, existing DNS settings are preserved. The scan takes up to about three seconds; connection time is only a rough latency estimate, not a DNS lookup or DNS-over-HTTPS performance test.
|
||||||
* [**Google**](https://developers.google.com/speed/public-dns?hl=en): A reliable and fast DNS service provided by Google.
|
* [**Google**](https://developers.google.com/speed/public-dns?hl=en): A reliable and fast DNS service provided by Google.
|
||||||
* [**Cloudflare**](https://developers.cloudflare.com/1.1.1.1/): Known for speed and privacy, Cloudflare DNS is a popular choice for enhancing internet performance.
|
* [**Cloudflare**](https://developers.cloudflare.com/1.1.1.1/): Known for speed and privacy, Cloudflare DNS is a popular choice for enhancing internet performance.
|
||||||
* [**Cloudflare_Malware**](https://developers.cloudflare.com/1.1.1.1/setup/#:~:text=Use%20the%20following%20DNS%20resolvers%20to%20block%20malicious%20content%3A): Provides additional protection by blocking malware sites.
|
* [**Cloudflare_Malware**](https://developers.cloudflare.com/1.1.1.1/setup/#:~:text=Use%20the%20following%20DNS%20resolvers%20to%20block%20malicious%20content%3A): Provides additional protection by blocking malware sites.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Changing modes adjusts system-wide Windows Update behavior. After switching mode
|
|||||||
- **Feature updates**: Delayed by **365 days** to reduce the chance of disruption from major Windows changes.
|
- **Feature updates**: Delayed by **365 days** to reduce the chance of disruption from major Windows changes.
|
||||||
- **Quality updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
|
- **Quality updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
|
||||||
- **Drivers**: Excluded from Windows quality updates.
|
- **Drivers**: Excluded from Windows quality updates.
|
||||||
- **Restarts**: Scheduled updates do not automatically restart Windows while a user is signed in. A restart explicitly scheduled by a user still takes precedence.
|
- **Installation**: Updates download automatically and notify you when they are ready to install. This setting does not control restarts after installation.
|
||||||
- **Availability**: Update deferral policies apply to Windows Pro, Enterprise, and Education editions.
|
- **Availability**: Update deferral policies apply to Windows Pro, Enterprise, and Education editions.
|
||||||
- **Why use it**: This mode offers the best balance between security and stability, which is why it is the recommended option for most PCs.
|
- **Why use it**: This mode offers the best balance between security and stability, which is why it is the recommended option for most PCs.
|
||||||
|
|
||||||
|
|||||||
@@ -23,42 +23,37 @@ This workflow is intended for fresh Windows installs, not in-place upgrades of a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
The tab is a three-step wizard. The step you are on is shown in the header row, and you can click an earlier step to go back to it until the image has been modified. The status log on the right stays visible throughout.
|
||||||
|
|
||||||
### Step 1 — Select Your Official Windows 11 ISO
|
### Step 1 — Select Your Official Windows 11 ISO
|
||||||
|
|
||||||
1. Open WinUtil and go to the **Win11 Creator** tab.
|
1. Open WinUtil and go to the **Win11 Creator** tab.
|
||||||
2. Click **Browse** and select your **official Windows 11 ISO file** from Microsoft (must be 4 GB or larger). Custom or modified ISOs are not supported.
|
2. Click **Browse** and select your **official Windows 11 ISO file** from Microsoft (must be 4 GB or larger). Custom or modified ISOs are not supported.
|
||||||
3. The file path and size will appear on screen once selected.
|
3. The file path and size will appear on screen once selected.
|
||||||
|
4. Click **Mount & Verify ISO**. WinUtil mounts the ISO, checks for a valid `install.wim` or `install.esd`, and reads the available editions (Home, Pro, Enterprise, etc.).
|
||||||
---
|
|
||||||
|
|
||||||
### Step 2 — Mount & Verify
|
|
||||||
|
|
||||||
1. Click **Mount & Verify ISO**.
|
|
||||||
2. WinUtil mounts the ISO, checks for a valid `install.wim` or `install.esd`, and reads the available editions (Home, Pro, Enterprise, etc.).
|
|
||||||
3. Once verified, select your desired **edition** from the dropdown — Pro is selected by default if available.
|
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
This step takes around 10–30 seconds, depending on your drive speed.
|
Mounting and verification takes around 10–30 seconds, depending on your drive speed.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 3 — Run the Modification
|
### Step 2 — Modify the Image
|
||||||
|
|
||||||
Click **Run Windows ISO Modification and Creator** to start the customization process. WinUtil will:
|
Once the ISO is verified, WinUtil moves to this step and shows the mounted drive and image file. Choose your **edition** from the dropdown — Pro is selected by default if available — and tick **Inject current system drivers** if you want them.
|
||||||
|
|
||||||
|
Then click **Run Windows ISO Modification and Creator** to start the customization process. WinUtil will:
|
||||||
|
|
||||||
**App & Component Removal:**
|
**App & Component Removal:**
|
||||||
- **Remove 40+ bloat apps** — Clipchamp, Teams, Copilot, Dev Home, new Outlook, Bing apps, Solitaire, and more
|
- **Remove 19 bloat apps** — Clipchamp, Teams, Dev Home, new Outlook, Bing apps, Solitaire, and more
|
||||||
- **Delete OneDrive setup** from the image
|
- **Uninstall OneDrive** during first logon
|
||||||
|
|
||||||
**System Customization:**
|
**System Customization:**
|
||||||
- **Bypass hardware checks** — removes TPM, Secure Boot, CPU, RAM, and storage requirement enforcement so the ISO installs on unsupported hardware
|
- **Bypass hardware checks** — removes TPM, Secure Boot, CPU, RAM, and storage requirement enforcement so the ISO installs on unsupported hardware
|
||||||
- **Enable local account setup** — injects an `autounattend.xml` that skips the Microsoft account screen during OOBE
|
- **Enable local account setup** — injects an `autounattend.xml` that skips the Microsoft account screen during OOBE
|
||||||
- **Disable BitLocker and device encryption** — removes startup overhead
|
- **Disable BitLocker and device encryption** — removes startup overhead
|
||||||
- **Disable Chat icon** — removes chat taskbar button
|
- **Disable Chat icon** — removes chat taskbar button
|
||||||
- **Strip unused editions** — keeps only your selected edition, saving 1–2 GB per removed edition
|
|
||||||
- **Pin the selected edition during setup** — writes setup metadata so OEM firmware keys for a different edition do not force the installer down the wrong product-key path
|
- **Pin the selected edition during setup** — writes setup metadata so OEM firmware keys for a different edition do not force the installer down the wrong product-key path
|
||||||
- **Clean the component store** — runs DISM cleanup to reclaim another 300–800 MB
|
|
||||||
|
|
||||||
**Privacy & Telemetry Tweaks:**
|
**Privacy & Telemetry Tweaks:**
|
||||||
- **Disable telemetry** — advertising ID, tailored experiences, input personalization, speech online privacy
|
- **Disable telemetry** — advertising ID, tailored experiences, input personalization, speech online privacy
|
||||||
@@ -72,13 +67,21 @@ Click **Run Windows ISO Modification and Creator** to start the customization pr
|
|||||||
- **Disable Copilot and search box suggestions**
|
- **Disable Copilot and search box suggestions**
|
||||||
|
|
||||||
**Optional: Driver Injection**
|
**Optional: Driver Injection**
|
||||||
- If enabled, it injects all drivers from your current system into the install.wim and boot.wim — useful for offline installations on machines with missing drivers. This is an optional checkbox in Step 3.
|
- If enabled, WinUtil exports the drivers from your current system, injects boot-storage drivers into `boot.wim` (Windows Setup, index 2), and injects eligible packages into the selected `install.wim` image. Stale duplicate packages are excluded before injection. Each remaining package is added separately, so one incompatible package does not stop the others. If a package fails, WinUtil discards the partial mount and retries the remaining packages against the original image. Check the live log for warnings: if every package fails, WinUtil keeps the original `install.wim` and still produces the ISO.
|
||||||
|
|
||||||
A live log shows progress as each step completes. This stage usually takes **10–30 minutes** depending on disk speed. The WIM dismount near the end is the slowest part, so do not close WinUtil while it is running.
|
:::note
|
||||||
|
Driver injection requires `sources\install.wim`. If the ISO uses `sources\install.esd`, leave this option off.
|
||||||
|
:::
|
||||||
|
|
||||||
|
A live log shows progress as each step completes. Without driver injection, WinUtil writes the setup changes in a few seconds after the ISO copy. With driver injection, this stage usually takes **10–20 minutes**, depending on hardware. Keep WinUtil open until the log reports completion.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
The resulting ISO is close to the size of the source ISO. WinUtil does not remove the unused editions from `install.wim`; it selects your edition through `sources\ei.cfg` and `autounattend.xml` so Windows Setup installs the right one.
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 4 — Export Your Result
|
### Step 3 — Export Your Result
|
||||||
|
|
||||||
Once the modification is complete, choose how to save your image:
|
Once the modification is complete, choose how to save your image:
|
||||||
|
|
||||||
@@ -96,7 +99,11 @@ Once the modification is complete, choose how to save your image:
|
|||||||
1. Click **Write Directly to a USB Drive**.
|
1. Click **Write Directly to a USB Drive**.
|
||||||
2. Select your USB drive from the dropdown (click **Refresh** if it doesn't appear).
|
2. Select your USB drive from the dropdown (click **Refresh** if it doesn't appear).
|
||||||
3. Click **Erase & Write to USB** and confirm the warning — **all data on the drive will be permanently erased**.
|
3. Click **Erase & Write to USB** and confirm the warning — **all data on the drive will be permanently erased**.
|
||||||
4. WinUtil formats the drive as GPT with a 512 MB EFI partition and copies the modified Windows files.
|
4. WinUtil formats the drive as GPT with a single FAT32 partition (capped at 32 GB, splitting `install.wim` into `.swm` files if larger than 3.8 GB) and copies the modified Windows files.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
WinUtil cannot split `install.esd`. If that file is 4 GB or larger, save an ISO instead of writing a FAT32 USB drive.
|
||||||
|
:::
|
||||||
|
|
||||||
:::danger
|
:::danger
|
||||||
Double-check you have selected the correct drive before confirming. This operation cannot be undone.
|
Double-check you have selected the correct drive before confirming. This operation cannot be undone.
|
||||||
@@ -108,9 +115,9 @@ Double-check you have selected the correct drive before confirming. This operati
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 5 — Clean Up (Optional)
|
### Start Over (Optional)
|
||||||
|
|
||||||
Click **Clean & Reset** to delete the temporary working directory (~10–15 GB) and return the tool to its initial state, ready for a new ISO. You will be asked to confirm before anything is deleted.
|
Click **Start Over** to delete the temporary working directory (~10–15 GB) and return the tool to its initial state, ready for a new ISO. You will be asked to confirm before anything is deleted.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -131,11 +138,13 @@ When you install Windows 11 from your modified ISO:
|
|||||||
|
|
||||||
| Problem | Fix |
|
| Problem | Fix |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| "install.wim not found" | Not a valid Windows 11 ISO — download a fresh one from Microsoft |
|
| "install.wim / install.esd was not found" | The ISO has no Windows installation image — download a fresh official ISO from Microsoft |
|
||||||
| "oscdimg.exe not found" | Run `winget install -e --id Microsoft.OSCDIMG` then retry |
|
| "oscdimg.exe not found" | Run `winget install -e --id Microsoft.OSCDIMG` then retry |
|
||||||
| USB drive not showing up | Plug it in, wait a few seconds, then click **Refresh** |
|
| USB drive not showing up | Plug it in, wait a few seconds, then click **Refresh** |
|
||||||
| Modification seems stuck | The WIM dismount step is slow — wait at least 10 minutes before assuming it's frozen |
|
| Driver injection seems stuck | WIM servicing can pause at a mount or commit. Allow 10–20 minutes depending on hardware, and check the live log before closing WinUtil |
|
||||||
| "Access Denied" error | Make sure WinUtil is running as Administrator |
|
| "Access Denied" error | Make sure WinUtil is running as Administrator |
|
||||||
|
| Driver injection warning | Review the live log for the named package. Other compatible packages continue; if none succeed, the ISO is created with the original `install.wim` |
|
||||||
|
| DISM error with an explanation in parentheses, e.g. "exit code 112 (Disk is full)" | WinUtil now explains common DISM failures in plain language. Free up disk space for a "Disk is full" error, or run WinUtil as Administrator for an "Access denied" error. If the code has no explanation, search the number on Microsoft's [DISM error code reference](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes) |
|
||||||
| "Setup has failed to validate the product key" | Recreate the ISO with the latest WinUtil. The creator now removes stale `PID.txt`, writes `sources\ei.cfg`, and pins the selected image in `autounattend.xml` so setup does not use an embedded OEM key for a different edition |
|
| "Setup has failed to validate the product key" | Recreate the ISO with the latest WinUtil. The creator now removes stale `PID.txt`, writes `sources\ei.cfg`, and pins the selected image in `autounattend.xml` so setup does not use an embedded OEM key for a different edition |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -27,6 +27,38 @@ hero:
|
|||||||
link: https://github.com/ChrisTitusTech/winutil
|
link: https://github.com/ChrisTitusTech/winutil
|
||||||
icon: external
|
icon: external
|
||||||
variant: secondary
|
variant: secondary
|
||||||
|
head:
|
||||||
|
- tag: meta
|
||||||
|
attrs:
|
||||||
|
property: og:title
|
||||||
|
content: Documentation | WinUtil
|
||||||
|
- tag: meta
|
||||||
|
attrs:
|
||||||
|
name: twitter:title
|
||||||
|
content: Documentation | WinUtil
|
||||||
|
- tag: script
|
||||||
|
attrs:
|
||||||
|
type: application/ld+json
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": "WinUtil",
|
||||||
|
"description": "Chris Titus Tech's Windows Utility — install apps, apply tweaks, run fixes, and manage Windows from one place.",
|
||||||
|
"url": "https://winutil.christitus.com/",
|
||||||
|
"downloadUrl": "https://github.com/ChrisTitusTech/winutil",
|
||||||
|
"operatingSystem": "Windows",
|
||||||
|
"applicationCategory": "UtilitiesApplication",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Chris Titus Tech"
|
||||||
|
}
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Icon, Code } from '@astrojs/starlight/components';
|
import { Icon, Code } from '@astrojs/starlight/components';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ If you run WinUtil and get an error like:
|
|||||||
|
|
||||||
`< : The term '<' is not recognized as the name of a cmdlet, function, script file, or operable program.`
|
`< : The term '<' is not recognized as the name of a cmdlet, function, script file, or operable program.`
|
||||||
|
|
||||||
try using a **VPN** and if that doesn't work than report the issue to https://github.com/ChrisTitusTech/winutil/issues
|
try using a **VPN** and if that doesn't work then report the issue to https://github.com/ChrisTitusTech/winutil/issues
|
||||||
|
|
||||||
### Script Won't Run
|
### Script Won't Run
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@ If you run WinUtil and get the error:
|
|||||||
|
|
||||||
`"WinUtil is unable to run on your system. PowerShell execution is restricted by security policies"`
|
`"WinUtil is unable to run on your system. PowerShell execution is restricted by security policies"`
|
||||||
|
|
||||||
this means that your PowerShell session is in **Constrained Language Mode**, which prevents WinUtil from running.
|
This means that your PowerShell session is in **Constrained Language Mode**, which prevents WinUtil from running.
|
||||||
|
|
||||||
### Ultimate Performance Plan Not Working
|
### Ultimate Performance Plan Not Working
|
||||||
|
|
||||||
The Ultimate Performance power plan may not work on some laptops that do not fully support this power plan.
|
The Ultimate Performance power plan may not work on some laptops that do not fully support this power plan.
|
||||||
|
|
||||||
In these cases, the power plan may fail to apply, This is expected behavior on unsupported hardware.
|
In these cases, the power plan may fail to apply. This is expected behavior on unsupported hardware.
|
||||||
|
|
||||||
### Revert start menu tweak not working
|
### Revert start menu tweak not working
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ In this update, Microsoft completely removed the old Start Menu code from Window
|
|||||||
### Issues with PowerShell 7 or Class not registered Error
|
### Issues with PowerShell 7 or Class not registered Error
|
||||||
Installing PowerShell 7 from the Microsoft Store (MSIX package) is known to cause issues with DISM cmdlets such as `Get-WindowsOptionalFeature` and `Enable-WindowsOptionalFeature`, resulting in a `Class not registered` COM error.
|
Installing PowerShell 7 from the Microsoft Store (MSIX package) is known to cause issues with DISM cmdlets such as `Get-WindowsOptionalFeature` and `Enable-WindowsOptionalFeature`, resulting in a `Class not registered` COM error.
|
||||||
|
|
||||||
This might also make it so running the "pre-installed app removal" will take a indefinite amount of time
|
This might also make it so running the "pre-installed app removal" will take an indefinite amount of time
|
||||||
|
|
||||||
Instead, install PowerShell 7 using one of the following methods:
|
Instead, install PowerShell 7 using one of the following methods:
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function Add-SelectedAppsMenuItem {
|
|||||||
$selectedAppRemoveButton.Add_MouseEnter({ $this.Foreground = "Red" })
|
$selectedAppRemoveButton.Add_MouseEnter({ $this.Foreground = "Red" })
|
||||||
$selectedAppRemoveButton.Add_MouseLeave({ $this.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "MainForegroundColor") })
|
$selectedAppRemoveButton.Add_MouseLeave({ $this.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "MainForegroundColor") })
|
||||||
$selectedAppRemoveButton.Add_Click({
|
$selectedAppRemoveButton.Add_Click({
|
||||||
$sync.($this.Tag).isChecked = $false # On click of the remove button, we only have to uncheck the corresponding checkbox. This will kick of all necessary changes to update the UI
|
$sync.($this.Tag).isChecked = $false # On click of the remove button, we only have to uncheck the corresponding checkbox. This will kick off all necessary changes to update the UI
|
||||||
})
|
})
|
||||||
[System.Windows.Controls.Grid]::SetColumn($selectedAppRemoveButton, 1)
|
[System.Windows.Controls.Grid]::SetColumn($selectedAppRemoveButton, 1)
|
||||||
$selectedAppGrid.Children.Add($selectedAppRemoveButton)
|
$selectedAppGrid.Children.Add($selectedAppRemoveButton)
|
||||||
|
|||||||
@@ -1,18 +1,147 @@
|
|||||||
function Close-WinUtilRunspacePool {
|
function Close-WinUtilRunspacePool {
|
||||||
if ($null -eq $sync -or -not $sync.ContainsKey("runspace") -or $null -eq $sync.runspace) {
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Stops anything still running and closes the worker pool
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Closing the pool with work still in it is what produced an unhandled
|
||||||
|
InvalidRunspaceStateException: a queued instance starts on a runspace that is already
|
||||||
|
closing, throws on a thread pool thread, and takes the process down. Whatever is in
|
||||||
|
flight is therefore asked to stop, and waited for, before the pool is closed.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[int]$StopTimeoutSeconds = 15,
|
||||||
|
|
||||||
|
# Leaves ShuttingDown clear: nothing resets it, so setting it here would refuse every
|
||||||
|
# later action for the rest of the session
|
||||||
|
[switch]$Recycle
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $sync) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$poolLock = Get-WinUtilRunspacePoolLock
|
||||||
|
[System.Threading.Monitor]::Enter($poolLock)
|
||||||
try {
|
try {
|
||||||
if ($sync.runspace.RunspacePoolStateInfo.State -notin @(
|
# Set before stopping, so nothing that is winding down queues fresh work behind us
|
||||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Closed,
|
if (-not $Recycle) {
|
||||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Closing,
|
$sync.ShuttingDown = $true
|
||||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Broken
|
}
|
||||||
)) {
|
|
||||||
$sync.runspace.Close()
|
if (-not $sync.ContainsKey("runspace") -or $null -eq $sync.runspace) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$stopped = $true
|
||||||
|
try {
|
||||||
|
$stopped = Stop-WinUtilActiveWork -TimeoutSeconds $StopTimeoutSeconds
|
||||||
|
} catch {
|
||||||
|
$stopped = $false
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "Could not stop running work cleanly: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$pool = $sync.runspace
|
||||||
|
$cleanupDeferred = $false
|
||||||
|
try {
|
||||||
|
$poolState = $pool.RunspacePoolStateInfo.State
|
||||||
|
$terminalStates = @(
|
||||||
|
[System.Management.Automation.Runspaces.RunspacePoolState]::Closed,
|
||||||
|
[System.Management.Automation.Runspaces.RunspacePoolState]::Broken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $stopped -and $poolState -notin $terminalStates) {
|
||||||
|
# Close and Dispose both wait for an invocation that ignored BeginStop. Hand
|
||||||
|
# cleanup to the thread pool so the timeout above remains a real upper bound.
|
||||||
|
$cleanupDeferred = $true
|
||||||
|
if ($poolState -ne [System.Management.Automation.Runspaces.RunspacePoolState]::Closing) {
|
||||||
|
Register-WinUtilRunspacePoolCleanup -RunspacePool $pool
|
||||||
|
}
|
||||||
|
} elseif ($poolState -notin ($terminalStates + [System.Management.Automation.Runspaces.RunspacePoolState]::Closing)) {
|
||||||
|
$pool.Close()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# A pool that will not close cleanly must not stop the window from closing
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "Worker pool did not close cleanly: $($_.Exception.Message)"
|
||||||
|
} finally {
|
||||||
|
if (-not $cleanupDeferred) {
|
||||||
|
try {
|
||||||
|
$pool.Dispose()
|
||||||
|
} catch {
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "Worker pool did not dispose cleanly: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sync.Remove("runspace")
|
||||||
|
if ($sync.ActiveShells) { $sync.ActiveShells.Clear() }
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$sync.runspace.Dispose()
|
[System.Threading.Monitor]::Exit($poolLock)
|
||||||
$sync.Remove("runspace")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Register-WinUtilRunspacePoolCleanup {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Closes and disposes a worker pool without blocking the calling thread
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[System.Management.Automation.Runspaces.RunspacePool]$RunspacePool
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not ("WinUtilRunspacePoolCleanup" -as [type])) {
|
||||||
|
Add-Type @"
|
||||||
|
using System;
|
||||||
|
using System.Management.Automation.Runspaces;
|
||||||
|
|
||||||
|
public sealed class WinUtilRunspacePoolCleanupState
|
||||||
|
{
|
||||||
|
public RunspacePool RunspacePool { get; set; }
|
||||||
|
public IAsyncResult Handle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WinUtilRunspacePoolCleanup
|
||||||
|
{
|
||||||
|
public static readonly System.Threading.WaitOrTimerCallback Callback = Cleanup;
|
||||||
|
|
||||||
|
public static void Cleanup(object state, bool timedOut)
|
||||||
|
{
|
||||||
|
var cleanupState = state as WinUtilRunspacePoolCleanupState;
|
||||||
|
if (cleanupState == null || cleanupState.RunspacePool == null || cleanupState.Handle == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cleanupState.RunspacePool.EndClose(cleanupState.Handle);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cleanupState.RunspacePool.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanupState = [WinUtilRunspacePoolCleanupState]::new()
|
||||||
|
$cleanupState.RunspacePool = $RunspacePool
|
||||||
|
$cleanupState.Handle = $RunspacePool.BeginClose($null, $null)
|
||||||
|
[System.Threading.ThreadPool]::RegisterWaitForSingleObject(
|
||||||
|
$cleanupState.Handle.AsyncWaitHandle,
|
||||||
|
[WinUtilRunspacePoolCleanup]::Callback,
|
||||||
|
$cleanupState,
|
||||||
|
-1,
|
||||||
|
$true
|
||||||
|
) | Out-Null
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
function Complete-WinUtilPackageRun {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Reports what a package run actually did and fails the job on unexpected errors
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Package managers report failure through an exit code, which is easy to walk past.
|
||||||
|
Without this the job layer would show a green checkmark for a run in which nothing
|
||||||
|
changed. Unexpected failures terminate the job; expected elevated-context skips
|
||||||
|
raise a warning so the job cannot claim that every requested action completed.
|
||||||
|
|
||||||
|
.PARAMETER Action
|
||||||
|
Install or Uninstall, used in the summary text.
|
||||||
|
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Action,
|
||||||
|
|
||||||
|
[object[]]$Results = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$succeeded = @($Results | Where-Object { $_.Outcome -eq "Succeeded" })
|
||||||
|
$skipped = @($Results | Where-Object { $_.Outcome -eq "Skipped" })
|
||||||
|
$failed = @($Results | Where-Object { $_.Outcome -eq "Failed" })
|
||||||
|
|
||||||
|
$summary = "$($succeeded.Count) succeeded, $($skipped.Count) skipped, $($failed.Count) failed"
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action summary: $summary"
|
||||||
|
Write-Host "$Action summary: $summary"
|
||||||
|
|
||||||
|
foreach ($result in $skipped) {
|
||||||
|
Write-Host " skipped $($result.Package) - $($result.Detail)"
|
||||||
|
}
|
||||||
|
foreach ($result in $failed) {
|
||||||
|
Write-Host " failed $($result.Package) - $($result.Detail)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminContextSkipped = @($skipped | Where-Object { $_.ExitCode -eq -1978335107 })
|
||||||
|
if ($adminContextSkipped.Count -gt 0) {
|
||||||
|
Write-Warning "$($adminContextSkipped.Count) package action(s) were skipped because elevated WinUtil cannot modify user-scoped installations."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failed.Count -gt 0) {
|
||||||
|
$names = ($failed | ForEach-Object { $_.Package }) -join ', '
|
||||||
|
$reasons = @($failed | ForEach-Object { $_.Detail } | Sort-Object -Unique)
|
||||||
|
|
||||||
|
$message = if ($reasons.Count -eq 1) {
|
||||||
|
"$($failed.Count) of $($Results.Count) package(s) failed: $names. $($reasons[0])"
|
||||||
|
} else {
|
||||||
|
"$($failed.Count) of $($Results.Count) package(s) failed: $names. See the lines above for each reason."
|
||||||
|
}
|
||||||
|
# Each failed package was already logged by its package-manager adapter. Carry that fact
|
||||||
|
# with the summary exception so the job wrapper adds context without another error count.
|
||||||
|
$exception = [System.InvalidOperationException]::new($message)
|
||||||
|
$exception.Data["WinUtilErrorReported"] = $true
|
||||||
|
throw $exception
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
function Find-AppsByNameOrDescription {
|
function Find-AppsByNameOrDescription {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Searches through the Apps on the Install Tab and hides all entries that do not match the string
|
Filters the Install tab entries by search text and by category
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Filters application entries by name or description using literal string matching.
|
Search text and categories are independent filters that both have to pass. An entry is
|
||||||
Respects collapsed category state and handles null $sync gracefully.
|
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
|
||||||
|
all the collapsed state the user set is restored.
|
||||||
|
|
||||||
.PARAMETER SearchString
|
.PARAMETER SearchString
|
||||||
The string to be searched for. Wildcards are treated as literal characters.
|
The string to search for. Wildcards are treated as literal characters.
|
||||||
|
|
||||||
.PARAMETER Category
|
.PARAMETER Categories
|
||||||
When provided, only applications in this exact category are shown.
|
The categories to show. An empty or missing array shows all of them.
|
||||||
|
|
||||||
.NOTES
|
.NOTES
|
||||||
- Uses module-scope $sync (no parameter needed; inherits from caller's scope)
|
- Uses module-scope $sync (no parameter needed; inherits from caller's scope)
|
||||||
- Performs literal matching (no wildcard expansion)
|
|
||||||
- Safely handles missing hashtable keys and null UI elements
|
- Safely handles missing hashtable keys and null UI elements
|
||||||
- Protected by try/catch to prevent UI thread crashes
|
- Protected by try/catch to prevent UI thread crashes
|
||||||
#>
|
#>
|
||||||
@@ -24,7 +29,7 @@ function Find-AppsByNameOrDescription {
|
|||||||
[string]$SearchString = "",
|
[string]$SearchString = "",
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$Category = ""
|
[string[]]$Categories = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate that $sync exists and has required structure
|
# Validate that $sync exists and has required structure
|
||||||
@@ -43,21 +48,34 @@ function Find-AppsByNameOrDescription {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Categories that filtering expanded on the user's behalf, so clearing the filter can undo it
|
||||||
|
if ($null -eq $sync.AppCategoryAutoExpanded) {
|
||||||
|
$sync.AppCategoryAutoExpanded = @{}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Reset the visibility if the search string is empty or the search is cleared
|
$activeCategories = @($Categories | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
if ([string]::IsNullOrWhiteSpace($SearchString) -and [string]::IsNullOrWhiteSpace($Category)) {
|
$hasSearch = -not [string]::IsNullOrWhiteSpace($SearchString)
|
||||||
|
$hasCategories = $activeCategories.Count -gt 0
|
||||||
|
|
||||||
|
# Nothing is filtered, so put every entry back and leave the collapsed categories collapsed
|
||||||
|
if (-not $hasSearch -and -not $hasCategories) {
|
||||||
$sync.ItemsControl.Items | ForEach-Object {
|
$sync.ItemsControl.Items | ForEach-Object {
|
||||||
# Each item is a StackPanel container
|
|
||||||
$_.Visibility = [Windows.Visibility]::Visible
|
$_.Visibility = [Windows.Visibility]::Visible
|
||||||
|
|
||||||
if ($_.Children.Count -ge 2) {
|
if ($_.Children.Count -ge 2) {
|
||||||
$categoryLabel = $_.Children[0]
|
$categoryLabel = $_.Children[0]
|
||||||
$wrapPanel = $_.Children[1]
|
$wrapPanel = $_.Children[1]
|
||||||
|
|
||||||
# Keep category label visible
|
|
||||||
$categoryLabel.Visibility = [Windows.Visibility]::Visible
|
$categoryLabel.Visibility = [Windows.Visibility]::Visible
|
||||||
|
|
||||||
# Respect the collapsed state of categories (indicated by + prefix)
|
# A category that filtering expanded goes back to how the user left it
|
||||||
|
$categoryName = $categoryLabel.Content -replace '^[+-] ', ''
|
||||||
|
if ($sync.AppCategoryAutoExpanded.ContainsKey($categoryName)) {
|
||||||
|
$categoryLabel.Content = $categoryLabel.Content -replace "^- ", "+ "
|
||||||
|
$sync.AppCategoryAutoExpanded.Remove($categoryName)
|
||||||
|
}
|
||||||
|
|
||||||
if ($categoryLabel.Content -like "+*") {
|
if ($categoryLabel.Content -like "+*") {
|
||||||
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
||||||
}
|
}
|
||||||
@@ -65,7 +83,6 @@ function Find-AppsByNameOrDescription {
|
|||||||
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show all apps within the category
|
|
||||||
$wrapPanel.Children | ForEach-Object {
|
$wrapPanel.Children | ForEach-Object {
|
||||||
$_.Visibility = [Windows.Visibility]::Visible
|
$_.Visibility = [Windows.Visibility]::Visible
|
||||||
}
|
}
|
||||||
@@ -77,7 +94,6 @@ function Find-AppsByNameOrDescription {
|
|||||||
# Escape wildcard characters for literal matching
|
# Escape wildcard characters for literal matching
|
||||||
$escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString)
|
$escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString)
|
||||||
|
|
||||||
# Perform search
|
|
||||||
$sync.ItemsControl.Items | ForEach-Object {
|
$sync.ItemsControl.Items | ForEach-Object {
|
||||||
# Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel
|
# Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel
|
||||||
if ($_.Children.Count -ge 2) {
|
if ($_.Children.Count -ge 2) {
|
||||||
@@ -85,12 +101,9 @@ function Find-AppsByNameOrDescription {
|
|||||||
$wrapPanel = $_.Children[1]
|
$wrapPanel = $_.Children[1]
|
||||||
$categoryHasMatch = $false
|
$categoryHasMatch = $false
|
||||||
|
|
||||||
# Keep category label visible
|
|
||||||
$categoryLabel.Visibility = [Windows.Visibility]::Visible
|
$categoryLabel.Visibility = [Windows.Visibility]::Visible
|
||||||
|
|
||||||
# Search through apps in this category
|
|
||||||
foreach ($appControl in $wrapPanel.Children) {
|
foreach ($appControl in $wrapPanel.Children) {
|
||||||
# Safely retrieve app entry from hashtable
|
|
||||||
$appTag = $appControl.Tag
|
$appTag = $appControl.Tag
|
||||||
$appEntry = $null
|
$appEntry = $null
|
||||||
|
|
||||||
@@ -98,14 +111,14 @@ function Find-AppsByNameOrDescription {
|
|||||||
$appEntry = $sync.configs.applicationsHashtable[$appTag]
|
$appEntry = $sync.configs.applicationsHashtable[$appTag]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if app matches search criteria
|
|
||||||
if ($null -ne $appEntry) {
|
if ($null -ne $appEntry) {
|
||||||
$categoryMatch = -not [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Category -eq $Category
|
$categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category
|
||||||
$contentMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Content -like "*$escapedSearchString*"
|
$textMatch = -not $hasSearch -or
|
||||||
$descriptionMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Description -like "*$escapedSearchString*"
|
$appEntry.Content -like "*$escapedSearchString*" -or
|
||||||
|
$appEntry.Description -like "*$escapedSearchString*" -or
|
||||||
|
$appTag -like "*$escapedSearchString*"
|
||||||
|
|
||||||
if ($categoryMatch -or $contentMatch -or $descriptionMatch) {
|
if ($categoryMatch -and $textMatch) {
|
||||||
# Show the App and mark that this category has a match
|
|
||||||
$appControl.Visibility = [Windows.Visibility]::Visible
|
$appControl.Visibility = [Windows.Visibility]::Visible
|
||||||
$categoryHasMatch = $true
|
$categoryHasMatch = $true
|
||||||
}
|
}
|
||||||
@@ -119,17 +132,17 @@ function Find-AppsByNameOrDescription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# If category has matches, show the WrapPanel and update the category label to expanded state
|
|
||||||
if ($categoryHasMatch) {
|
if ($categoryHasMatch) {
|
||||||
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
||||||
$_.Visibility = [Windows.Visibility]::Visible
|
$_.Visibility = [Windows.Visibility]::Visible
|
||||||
# Update category label to show expanded state (-)
|
# Expand it, otherwise the matches stay hidden behind a collapsed header.
|
||||||
|
# Remember that it was collapsed so clearing the filter can put it back.
|
||||||
if ($categoryLabel.Content -like "+*") {
|
if ($categoryLabel.Content -like "+*") {
|
||||||
$categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- "
|
$categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- "
|
||||||
|
$sync.AppCategoryAutoExpanded[($categoryLabel.Content -replace '^- ', '')] = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Hide the entire category container if no matches
|
|
||||||
$_.Visibility = [Windows.Visibility]::Collapsed
|
$_.Visibility = [Windows.Visibility]::Collapsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,18 +90,25 @@ function Find-TweaksByNameOrDescription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
||||||
$itemsControl = $null
|
$container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1
|
||||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
|
|
||||||
|
|
||||||
if ($null -ne $itemsControl) {
|
if ($null -ne $container) {
|
||||||
|
$targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container }
|
||||||
|
$items = $null
|
||||||
|
if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") {
|
||||||
|
$items = $targetPanel.Items
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$items = $targetPanel.Children
|
||||||
|
}
|
||||||
# Show all items in the category
|
# Show all items in the category
|
||||||
foreach ($item in $itemsControl.Items) {
|
foreach ($item in $items) {
|
||||||
if ($null -ne $item) {
|
if ($null -ne $item) {
|
||||||
# Check if it's a category label (first Label in the ItemsControl)
|
# Check if it's a category label (first Label in the container)
|
||||||
if ($item -is [Windows.Controls.Label]) {
|
if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") {
|
||||||
$item.Visibility = [Windows.Visibility]::Visible
|
$item.Visibility = [Windows.Visibility]::Visible
|
||||||
}
|
}
|
||||||
elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) {
|
elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "DockPanel" -or $item.GetType().Name -eq "StackPanel") {
|
||||||
# Show all checkbox containers
|
# Show all checkbox containers
|
||||||
$item.Visibility = [Windows.Visibility]::Visible
|
$item.Visibility = [Windows.Visibility]::Visible
|
||||||
}
|
}
|
||||||
@@ -143,16 +150,21 @@ function Find-TweaksByNameOrDescription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
||||||
$itemsControl = $null
|
$container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1
|
||||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
|
|
||||||
|
|
||||||
if ($null -ne $itemsControl) {
|
if ($null -ne $container) {
|
||||||
$categoryLabel = $null
|
$categoryLabel = $null
|
||||||
|
|
||||||
# Process all items (checkboxes, labels, panels) in the ItemsControl
|
$targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container }
|
||||||
for ($i = 0; $i -lt $itemsControl.Items.Count; $i++) {
|
$items = $null
|
||||||
$item = $itemsControl.Items[$i]
|
if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") {
|
||||||
|
$items = $targetPanel.Items
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$items = $targetPanel.Children
|
||||||
|
}
|
||||||
|
# Process all items (checkboxes, labels, panels) in the container
|
||||||
|
foreach ($item in $items) {
|
||||||
if ($null -eq $item) {
|
if ($null -eq $item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -161,7 +173,7 @@ function Find-TweaksByNameOrDescription {
|
|||||||
# Check if this is a category label (usually first Label)
|
# Check if this is a category label (usually first Label)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
if ($item -is [Windows.Controls.Label]) {
|
if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") {
|
||||||
$categoryLabel = $item
|
$categoryLabel = $item
|
||||||
# Initially hide category label; show it only if matches found
|
# Initially hide category label; show it only if matches found
|
||||||
$item.Visibility = [Windows.Visibility]::Collapsed
|
$item.Visibility = [Windows.Visibility]::Collapsed
|
||||||
@@ -171,13 +183,13 @@ function Find-TweaksByNameOrDescription {
|
|||||||
# Check if this is a DockPanel containing a tweak checkbox
|
# Check if this is a DockPanel containing a tweak checkbox
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
elseif ($item -is [Windows.Controls.DockPanel]) {
|
elseif ($item -is [Windows.Controls.DockPanel] -or $item.GetType().Name -eq "DockPanel") {
|
||||||
$checkbox = $null
|
$checkbox = $null
|
||||||
$label = $null
|
$label = $null
|
||||||
|
|
||||||
# Safely extract checkbox and label
|
# Safely extract checkbox and label
|
||||||
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
|
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1
|
||||||
$label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1
|
$label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] -or $_.GetType().Name -eq "Label" } | Select-Object -First 1
|
||||||
|
|
||||||
# Check if tweak matches search criteria
|
# Check if tweak matches search criteria
|
||||||
$itemMatches = $false
|
$itemMatches = $false
|
||||||
@@ -221,9 +233,9 @@ function Find-TweaksByNameOrDescription {
|
|||||||
# Check if this is a StackPanel containing a tweak checkbox
|
# Check if this is a StackPanel containing a tweak checkbox
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
elseif ($item -is [Windows.Controls.StackPanel]) {
|
elseif ($item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "StackPanel") {
|
||||||
$checkbox = $null
|
$checkbox = $null
|
||||||
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
|
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1
|
||||||
|
|
||||||
$itemMatches = $false
|
$itemMatches = $false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
function Get-WinUtilAppEntryHandlers {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
The event handlers shared by every app entry on the Install tab
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
A scriptblock literal inside a loop is a new scriptblock every time round, and
|
||||||
|
building six of them per app is the single largest cost of drawing the app list:
|
||||||
|
measured at 2.13 ms per entry against 0.62 ms when they are made once and reused.
|
||||||
|
|
||||||
|
None of them close over anything per entry. They read the sender through $this, so
|
||||||
|
one instance serves every app.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if ($null -ne $script:WinUtilAppEntryHandlers) {
|
||||||
|
return $script:WinUtilAppEntryHandlers
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:WinUtilAppEntryHandlers = @{
|
||||||
|
BorderClick = {
|
||||||
|
# Resolve through $sync because the border's child is a layout Grid for FOSS entries
|
||||||
|
$childCheckbox = $sync.$($this.Tag)
|
||||||
|
$childCheckbox.IsChecked = -not $childCheckbox.IsChecked
|
||||||
|
}
|
||||||
|
MouseEnter = {
|
||||||
|
if (($sync.$($this.Tag).IsChecked) -eq $false) {
|
||||||
|
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallHighlightedColor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseLeave = {
|
||||||
|
if (($sync.$($this.Tag).IsChecked) -eq $false) {
|
||||||
|
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RightClick = {
|
||||||
|
# Store the selected app in a global variable so it can be used in the popup
|
||||||
|
$sync.appPopupSelectedApp = $this.Tag
|
||||||
|
# Set the popup position to the current mouse position
|
||||||
|
$sync.appPopup.PlacementTarget = $this
|
||||||
|
$sync.appPopup.IsOpen = $true
|
||||||
|
}
|
||||||
|
# The checkbox sits inside the entry layout Grid, so the border is one level further up
|
||||||
|
Checked = {
|
||||||
|
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $this.Tag
|
||||||
|
$borderElement = $this.Parent.Parent
|
||||||
|
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallSelectedColor")
|
||||||
|
}
|
||||||
|
Unchecked = {
|
||||||
|
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $this.Tag
|
||||||
|
$borderElement = $this.Parent.Parent
|
||||||
|
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
|
||||||
|
}
|
||||||
|
ImageFailed = {
|
||||||
|
$this.Visibility = "Collapsed"
|
||||||
|
$this.Parent.Children[0].Visibility = "Visible"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $script:WinUtilAppEntryHandlers
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
function Get-WinUtilDNSBenchmark {
|
||||||
|
<#
|
||||||
|
|
||||||
|
.SYNOPSIS
|
||||||
|
Benchmarks neutral DNS providers by measuring TCP port 53 latency (RTT in ms) to determine the fastest DNS server.
|
||||||
|
|
||||||
|
.PARAMETER TimeoutMs
|
||||||
|
Maximum timeout in milliseconds for each connection test. Default is 1500ms.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
Array of PSCustomObjects containing Provider, PrimaryIP, and LatencyMs sorted by lowest latency.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$results = Get-WinUtilDNSBenchmark
|
||||||
|
$fastest = $results[0]
|
||||||
|
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateRange(1, 9998)]
|
||||||
|
[int]$TimeoutMs = 1500
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "Starting DNS latency benchmark scan (TCP port 53)..."
|
||||||
|
|
||||||
|
$dnsConfigs = $sync.configs.dns
|
||||||
|
if ($null -eq $dnsConfigs) {
|
||||||
|
Write-Warning "DNS configurations not found in `$sync.configs.dns."
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS configurations not found in `$sync.configs.dns."
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [System.Collections.Generic.List[PSObject]]::new()
|
||||||
|
|
||||||
|
foreach ($prop in $dnsConfigs.PSObject.Properties) {
|
||||||
|
$providerName = $prop.Name
|
||||||
|
$primaryIp = $prop.Value.Primary
|
||||||
|
if (-not $primaryIp) { continue }
|
||||||
|
|
||||||
|
# Providers must explicitly opt in so new filtering services are never auto-selected.
|
||||||
|
if ($prop.Value.BenchmarkEligible -ne $true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$latency = 9999
|
||||||
|
$client = $null
|
||||||
|
try {
|
||||||
|
$client = New-Object System.Net.Sockets.TcpClient
|
||||||
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
$asyncResult = $client.BeginConnect($primaryIp, 53, $null, $null)
|
||||||
|
$success = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
||||||
|
$stopwatch.Stop()
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$client.EndConnect($asyncResult)
|
||||||
|
$latency = [int]$stopwatch.ElapsedMilliseconds
|
||||||
|
} else {
|
||||||
|
$latency = 9999
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$latency = 9999
|
||||||
|
} finally {
|
||||||
|
if ($null -ne $client) {
|
||||||
|
$client.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results.Add([PSCustomObject]@{
|
||||||
|
Provider = $providerName
|
||||||
|
PrimaryIP = $primaryIp
|
||||||
|
LatencyMs = $latency
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortedResults = @($results | Sort-Object LatencyMs)
|
||||||
|
if ($sortedResults.Count -gt 0 -and $sortedResults[0].LatencyMs -lt 9999) {
|
||||||
|
$fastest = $sortedResults[0]
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "DNS Benchmark completed. Fastest neutral provider: $($fastest.Provider) ($($fastest.LatencyMs) ms)"
|
||||||
|
} else {
|
||||||
|
Write-WinUtilLog -Component "DNS" -Message "DNS Benchmark completed. Could not determine latency for providers."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sortedResults
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
function Get-WinUtilEnvironmentReport {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Collects the allowlisted data used by the WinUtil environment report.
|
||||||
|
#>
|
||||||
|
|
||||||
|
$reportWarnings = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
$windows = [ordered]@{
|
||||||
|
edition = $null
|
||||||
|
version = $null
|
||||||
|
buildNumber = $null
|
||||||
|
architecture = $null
|
||||||
|
}
|
||||||
|
$hardware = [ordered]@{
|
||||||
|
cpuModel = $null
|
||||||
|
logicalProcessorCount = $null
|
||||||
|
totalMemoryGB = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
|
||||||
|
$windows.edition = $operatingSystem.Caption
|
||||||
|
$windows.version = $operatingSystem.Version
|
||||||
|
$windows.buildNumber = $operatingSystem.BuildNumber
|
||||||
|
$windows.architecture = $operatingSystem.OSArchitecture
|
||||||
|
|
||||||
|
if ($null -ne $operatingSystem.TotalVisibleMemorySize) {
|
||||||
|
$hardware.totalMemoryGB = [math]::Round(([double]$operatingSystem.TotalVisibleMemorySize / 1MB), 2)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to collect Windows/memory info from Win32_OperatingSystem: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$processors = @(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop)
|
||||||
|
if ($processors.Count -gt 0) {
|
||||||
|
$hardware.cpuModel = $processors[0].Name
|
||||||
|
$hardware.logicalProcessorCount = [int](($processors | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to collect CPU info from Win32_Processor: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
$powershell = [ordered]@{
|
||||||
|
edition = $PSVersionTable.PSEdition
|
||||||
|
version = $PSVersionTable.PSVersion.ToString()
|
||||||
|
executionPolicy = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$powershell.executionPolicy = (Get-ExecutionPolicy).ToString()
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to read PowerShell execution policy: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-use built-in functionality
|
||||||
|
$chocolatey = [ordered]@{ installed = $false; version = $null }
|
||||||
|
try {
|
||||||
|
$chocolatey.installed = (Test-WinUtilPackageManager -choco 6>$null) -eq "installed"
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to check Chocolatey availability: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($chocolatey.installed) {
|
||||||
|
try {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
$versionOutput = @(choco -v 2>&1)
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Chocolatey version probe exited with code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
$chocolatey.version = ($versionOutput | Select-Object -First 1).ToString().Trim()
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to read Chocolatey version: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$winget = [ordered]@{ installed = $false; version = $null }
|
||||||
|
try {
|
||||||
|
$winget.installed = (Test-WinUtilPackageManager -winget 6>$null) -eq "installed"
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to check WinGet availability: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($winget.installed) {
|
||||||
|
try {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
$versionOutput = @(winget -v 2>&1)
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "WinGet version probe exited with code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
$winget.version = ($versionOutput | Select-Object -First 1).ToString().Trim()
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to read WinGet version: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Null means the registry state could not be read. Do not turn an access/provider failure into
|
||||||
|
# a misleading "no reboot required" result.
|
||||||
|
$system = [ordered]@{ pendingRebootRequired = $null }
|
||||||
|
try {
|
||||||
|
$rebootPaths = @(
|
||||||
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending",
|
||||||
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A present-but-empty PendingFileRenameOperations value still returns a non-null object, so
|
||||||
|
# check the actual entries rather than just whether the property exists.
|
||||||
|
$pendingFileRenameOperations = @(
|
||||||
|
(Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" `
|
||||||
|
-ErrorAction Stop).PendingFileRenameOperations |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }
|
||||||
|
)
|
||||||
|
$system.pendingRebootRequired = ($rebootPaths | Where-Object {
|
||||||
|
Test-Path -LiteralPath $_ -ErrorAction Stop
|
||||||
|
}).Count -gt 0 -or
|
||||||
|
$pendingFileRenameOperations.Count -gt 0
|
||||||
|
} catch {
|
||||||
|
$message = "Failed to check pending-reboot registry state: $($_.Exception.Message)"
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
$tweaksState = Get-WinUtilTweaksStateReport
|
||||||
|
if ($tweaksState.collectionStatus -ne "collected") {
|
||||||
|
$message = "Failed to collect the complete tweak state for the environment report."
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message $message
|
||||||
|
[void]$reportWarnings.Add($message)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($message in $reportWarnings) {
|
||||||
|
Write-Warning $message
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject][ordered]@{
|
||||||
|
schemaVersion = "1.0"
|
||||||
|
generatedAtUtc = [DateTime]::UtcNow.ToString("o")
|
||||||
|
windows = [pscustomobject]$windows
|
||||||
|
hardware = [pscustomobject]$hardware
|
||||||
|
powershell = [pscustomobject]$powershell
|
||||||
|
packageManagers = [pscustomobject][ordered]@{
|
||||||
|
winget = [pscustomobject]$winget
|
||||||
|
chocolatey = [pscustomobject]$chocolatey
|
||||||
|
}
|
||||||
|
system = [pscustomobject]$system
|
||||||
|
tweaksState = $tweaksState
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
function Get-WinUtilEnvironmentReportLogsPath {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Derives the companion logs .txt path from the environment report's JSON save path.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$JsonPath
|
||||||
|
)
|
||||||
|
|
||||||
|
# ChangeExtension($JsonPath, $null) leaves a trailing dot instead of stripping it, so build the
|
||||||
|
# name from its parts instead.
|
||||||
|
$directory = [System.IO.Path]::GetDirectoryName($JsonPath)
|
||||||
|
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($JsonPath)
|
||||||
|
return [System.IO.Path]::Combine($directory, "${baseName}_logs.txt")
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
function Get-WinUtilRecentLogs {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Concatenates WinUtil session logs from the last N days into a single text blob.
|
||||||
|
|
||||||
|
.PARAMETER Days
|
||||||
|
How many days back to include. Defaults to 7, matching what the support forum/server
|
||||||
|
typically asks users for.
|
||||||
|
|
||||||
|
.PARAMETER LogDirectory
|
||||||
|
Overrides the log directory (normally $sync.winutildir\logs). Mainly for testing.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[int]$Days = 7,
|
||||||
|
[string]$LogDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($LogDirectory)) {
|
||||||
|
if ($null -eq $sync -or -not $sync.ContainsKey("winutildir") -or [string]::IsNullOrWhiteSpace($sync.winutildir)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
$LogDirectory = Join-Path $sync.winutildir "logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $LogDirectory -ErrorAction Stop)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$cutoff = (Get-Date).AddDays(-$Days)
|
||||||
|
$logFiles = Get-ChildItem -LiteralPath $LogDirectory -Filter "winutil_*.log" -File -ErrorAction Stop |
|
||||||
|
Where-Object { $_.LastWriteTime -ge $cutoff } |
|
||||||
|
Sort-Object LastWriteTime
|
||||||
|
|
||||||
|
$sections = foreach ($logFile in $logFiles) {
|
||||||
|
"=== $($logFile.Name) ===`n$(Get-Content -LiteralPath $logFile.FullName -Raw -ErrorAction Stop)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($sections -join "`n`n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
function Get-WinUtilRegistryComboState {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Finds the configured combo-box state matching the current registry values.
|
||||||
|
|
||||||
|
.PARAMETER Registry
|
||||||
|
Registry settings containing a value mapping for each supported state.
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
The name of the matching state.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Registry
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($state in $Registry[0].Values.PSObject.Properties) {
|
||||||
|
$stateMatches = $true
|
||||||
|
foreach ($setting in @($Registry)) {
|
||||||
|
$currentValue = Get-WinUtilRegistryComboValue -Setting $setting
|
||||||
|
$actualValue = if ($currentValue.Exists -and $null -ne $currentValue.Value) { $currentValue.Value } else { $setting.DefaultValue }
|
||||||
|
$configuredValue = $setting.Values.PSObject.Properties[$state.Name].Value
|
||||||
|
# Removal represents the effective Windows default when matching the current state.
|
||||||
|
$expectedValue = if ($configuredValue -eq "<RemoveEntry>") { $setting.DefaultValue } else { $configuredValue }
|
||||||
|
if ([string]$actualValue -ne [string]$expectedValue) {
|
||||||
|
$stateMatches = $false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($stateMatches) {
|
||||||
|
return $state.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Registry values do not match a supported state."
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
function Get-WinUtilRegistryComboValue {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Reads one registry value for a registry-backed combo-box state.
|
||||||
|
|
||||||
|
.PARAMETER Setting
|
||||||
|
The registry setting from the combo-box configuration.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Setting
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$item = Get-ItemProperty -Path $Setting.Path -Name $Setting.Name -ErrorAction Stop
|
||||||
|
$property = $item.PSObject.Properties[$Setting.Name]
|
||||||
|
return [pscustomobject]@{ Exists = $null -ne $property; Value = $property.Value }
|
||||||
|
} catch [System.Management.Automation.PSArgumentException] {
|
||||||
|
# The registry provider uses PSArgumentException when a named value is absent.
|
||||||
|
return [pscustomobject]@{ Exists = $false; Value = $null }
|
||||||
|
} catch [System.Management.Automation.ItemNotFoundException] {
|
||||||
|
return [pscustomobject]@{ Exists = $false; Value = $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
function Get-WinUtilRunspacePoolLock {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Returns the lock that serializes worker-pool startup and shutdown
|
||||||
|
#>
|
||||||
|
|
||||||
|
[System.Threading.Monitor]::Enter($sync.SyncRoot)
|
||||||
|
try {
|
||||||
|
if ($null -eq $sync.RunspacePoolLock) {
|
||||||
|
$sync.RunspacePoolLock = [object]::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sync.RunspacePoolLock
|
||||||
|
} finally {
|
||||||
|
[System.Threading.Monitor]::Exit($sync.SyncRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,9 @@ function Get-WinUtilSelectedPackages {
|
|||||||
[string] $Preference
|
[string] $Preference
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A single package has no meaningful percentage to show
|
||||||
if ($PackageList.count -eq 1) {
|
if ($PackageList.count -eq 1) {
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
|
Step-WinUtilJob -State "Indeterminate"
|
||||||
} else {
|
|
||||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$packagesWinget = [System.Collections.ArrayList]::new()
|
$packagesWinget = [System.Collections.ArrayList]::new()
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
Function Get-WinUtilToggleStatus ($ToggleSwitch) {
|
Function Get-WinUtilToggleStatus {
|
||||||
|
param(
|
||||||
|
$ToggleSwitch,
|
||||||
|
[switch]$BypassCache,
|
||||||
|
[switch]$StopOnReadError
|
||||||
|
)
|
||||||
|
|
||||||
$ToggleSwitchReg = $sync.configs.tweaks.$ToggleSwitch.registry
|
$ToggleSwitchReg = $sync.configs.tweaks.$ToggleSwitch.registry
|
||||||
|
|
||||||
if ($null -eq $sync.ToggleStatusCache) {
|
if (-not $BypassCache) {
|
||||||
$sync.ToggleStatusCache = @{}
|
if ($null -eq $sync.ToggleStatusCache) {
|
||||||
|
$sync.ToggleStatusCache = @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.ToggleStatusCache.ContainsKey($ToggleSwitch)) {
|
||||||
|
return [bool]$sync.ToggleStatusCache[$ToggleSwitch]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sync.ToggleStatusCache.ContainsKey($ToggleSwitch)) {
|
$readErrorAction = if ($StopOnReadError) { "Stop" } else { "Continue" }
|
||||||
return [bool]$sync.ToggleStatusCache[$ToggleSwitch]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
||||||
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null
|
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS -ErrorAction $readErrorAction | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($regentry in $ToggleSwitchReg) {
|
foreach ($regentry in $ToggleSwitchReg) {
|
||||||
|
|
||||||
if (Test-Path $regentry.Path) {
|
if (Test-Path $regentry.Path -ErrorAction $readErrorAction) {
|
||||||
$regstate = (Get-ItemProperty -Path $regentry.Path).$($regentry.Name)
|
$regstate = (Get-ItemProperty -Path $regentry.Path -ErrorAction $readErrorAction).$($regentry.Name)
|
||||||
} else {
|
} else {
|
||||||
$regstate = $null
|
$regstate = $null
|
||||||
}
|
}
|
||||||
@@ -30,11 +39,15 @@ Function Get-WinUtilToggleStatus ($ToggleSwitch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($regstate -ne $regentry.Value) {
|
if ($regstate -ne $regentry.Value) {
|
||||||
$sync.ToggleStatusCache[$ToggleSwitch] = $false
|
if (-not $BypassCache) {
|
||||||
|
$sync.ToggleStatusCache[$ToggleSwitch] = $false
|
||||||
|
}
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sync.ToggleStatusCache[$ToggleSwitch] = $true
|
if (-not $BypassCache) {
|
||||||
|
$sync.ToggleStatusCache[$ToggleSwitch] = $true
|
||||||
|
}
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
function Get-WinUtilTweaksStateReport {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Groups every config/tweaks.json entry's live applied state by category, reusing the same
|
||||||
|
detection Invoke-WPFGetInstalled uses to check the "Select Installed Tweaks" checkboxes.
|
||||||
|
#>
|
||||||
|
|
||||||
|
$categoryFieldNames = [ordered]@{
|
||||||
|
"Essential Tweaks" = "essentialTweaks"
|
||||||
|
"Customize Preferences" = "customizePreferences"
|
||||||
|
"z__Advanced Tweaks - CAUTION" = "advancedTweaks"
|
||||||
|
"Performance Plans - NOT FOR LAPTOPS" = "performancePlans"
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped = [ordered]@{}
|
||||||
|
foreach ($fieldName in $categoryFieldNames.Values) {
|
||||||
|
$grouped[$fieldName] = [ordered]@{}
|
||||||
|
}
|
||||||
|
$notEvaluable = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$collectionStatus = "collected"
|
||||||
|
|
||||||
|
try {
|
||||||
|
$appliedTweaks = [System.Collections.Generic.HashSet[string]]::new(
|
||||||
|
[string[]]@(Invoke-WinUtilCurrentSystem -CheckBox "tweaks" `
|
||||||
|
-BypassToggleStatusCache -StopOnReadError)
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($property in $sync.configs.tweaks.PSObject.Properties) {
|
||||||
|
$tweakKey = $property.Name
|
||||||
|
$entry = $property.Value
|
||||||
|
$fieldName = $categoryFieldNames[[string]$entry.category]
|
||||||
|
|
||||||
|
# Buttons embedded in the tweaks panel (e.g. the OOSU/Ultimate Performance launchers)
|
||||||
|
# are actions, not stateful tweaks, so they're outside this report's scope entirely.
|
||||||
|
if (-not $fieldName -or $entry.Type -eq "Button") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Combobox tweaks and script-only tweaks with no registry/service schema have no
|
||||||
|
# detectable current state. List them so the report doesn't silently drop them.
|
||||||
|
if ($entry.Type -eq "Combobox" -or (-not $entry.registry -and -not $entry.service)) {
|
||||||
|
$notEvaluable.Add($tweakKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped[$fieldName][$tweakKey] = $appliedTweaks.Contains($tweakKey)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-WinUtilLog -Component "EnvironmentReport" -Level "WARN" -Message "Failed to collect tweaks/toggle state: $($_.Exception.Message)"
|
||||||
|
$collectionStatus = "unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Empty groups from a failed collection would otherwise be indistinguishable in the JSON from a
|
||||||
|
# successful scan that found nothing notable, so record whether collection actually ran.
|
||||||
|
$result = [ordered]@{ collectionStatus = $collectionStatus }
|
||||||
|
foreach ($fieldName in $categoryFieldNames.Values) {
|
||||||
|
$result[$fieldName] = [pscustomobject]$grouped[$fieldName]
|
||||||
|
}
|
||||||
|
$result.notEvaluable = @($notEvaluable)
|
||||||
|
|
||||||
|
return [pscustomobject]$result
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Creates a [Windows.Controls.ScrollViewer] containing a [Windows.Controls.ItemsControl] which is setup to use Virtualization to only load the visible elements for performance reasons.
|
Creates a [Windows.Controls.ScrollViewer] containing a [Windows.Controls.ItemsControl] which is setup to use Virtualization to only load the visible elements for performance reasons.
|
||||||
This is used as the parent object for all category and app entries on the install tab
|
This is used as the parent object for all category and app entries on the install tab
|
||||||
Used to as part of the Install Tab UI generation
|
Used as part of the Install Tab UI generation
|
||||||
|
|
||||||
.PARAMETER TargetElement
|
.PARAMETER TargetElement
|
||||||
The element to which the AppArea should be added
|
The element to which the AppArea should be added
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
$targetGrid = $sync.Form.FindName($TargetElement)
|
$targetGrid = $sync.Form.FindName($TargetElement)
|
||||||
$null = $targetGrid.Children.Clear()
|
$null = $targetGrid.Children.Clear()
|
||||||
|
|
||||||
# Create the outer Border for the aren where the apps will be placed
|
# Create the outer Border for the area where the apps will be placed
|
||||||
$Border = New-Object Windows.Controls.Border
|
$Border = New-Object Windows.Controls.Border
|
||||||
$Border.VerticalAlignment = "Stretch"
|
$Border.VerticalAlignment = "Stretch"
|
||||||
$Border.SetResourceReference([Windows.Controls.Control]::StyleProperty, "BorderStyle")
|
$Border.SetResourceReference([Windows.Controls.Control]::StyleProperty, "BorderStyle")
|
||||||
|
|||||||
@@ -13,34 +13,18 @@ function Initialize-InstallAppEntry {
|
|||||||
$appKey
|
$appKey
|
||||||
)
|
)
|
||||||
|
|
||||||
$app = $sync.configs.applicationsHashtable.$appKey
|
$app = $sync.configs.applicationsHashtable[$appKey]
|
||||||
|
$handlers = Get-WinUtilAppEntryHandlers
|
||||||
|
|
||||||
# Create the outer Border for the application type
|
# Create the outer Border for the application type
|
||||||
$border = New-Object Windows.Controls.Border
|
$border = New-Object Windows.Controls.Border
|
||||||
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
|
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
|
||||||
$border.Tag = $appKey
|
$border.Tag = $appKey
|
||||||
$border.ToolTip = $app.description
|
$border.ToolTip = Get-WinUtilEntryToolTip -Description $app.description -Key $appKey
|
||||||
$border.Add_MouseLeftButtonUp({
|
$border.Add_MouseLeftButtonUp($handlers.BorderClick)
|
||||||
$childCheckbox = ($this.Child | Where-Object {$_.Template.TargetType -eq [System.Windows.Controls.Checkbox]})[0]
|
$border.Add_MouseEnter($handlers.MouseEnter)
|
||||||
$childCheckBox.isChecked = -not $childCheckbox.IsChecked
|
$border.Add_MouseLeave($handlers.MouseLeave)
|
||||||
})
|
$border.Add_MouseRightButtonUp($handlers.RightClick)
|
||||||
$border.Add_MouseEnter({
|
|
||||||
if (($sync.$($this.Tag).IsChecked) -eq $false) {
|
|
||||||
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallHighlightedColor")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$border.Add_MouseLeave({
|
|
||||||
if (($sync.$($this.Tag).IsChecked) -eq $false) {
|
|
||||||
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
$border.Add_MouseRightButtonUp({
|
|
||||||
# Store the selected app in a global variable so it can be used in the popup
|
|
||||||
$sync.appPopupSelectedApp = $this.Tag
|
|
||||||
# Set the popup position to the current mouse position
|
|
||||||
$sync.appPopup.PlacementTarget = $this
|
|
||||||
$sync.appPopup.IsOpen = $true
|
|
||||||
})
|
|
||||||
|
|
||||||
$checkBox = New-Object Windows.Controls.CheckBox
|
$checkBox = New-Object Windows.Controls.CheckBox
|
||||||
# Sanitize the name for WPF
|
# Sanitize the name for WPF
|
||||||
@@ -48,17 +32,8 @@ function Initialize-InstallAppEntry {
|
|||||||
# Store the original appKey in Tag
|
# Store the original appKey in Tag
|
||||||
$checkBox.Tag = $appKey
|
$checkBox.Tag = $appKey
|
||||||
$checkbox.Style = $sync.Form.Resources.AppEntryCheckboxStyle
|
$checkbox.Style = $sync.Form.Resources.AppEntryCheckboxStyle
|
||||||
$checkbox.Add_Checked({
|
$checkbox.Add_Checked($handlers.Checked)
|
||||||
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $this.Parent.Tag
|
$checkbox.Add_Unchecked($handlers.Unchecked)
|
||||||
$borderElement = $this.Parent
|
|
||||||
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallSelectedColor")
|
|
||||||
})
|
|
||||||
|
|
||||||
$checkbox.Add_Unchecked({
|
|
||||||
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $this.Parent.Tag
|
|
||||||
$borderElement = $this.Parent
|
|
||||||
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
|
|
||||||
})
|
|
||||||
|
|
||||||
$contentPanel = New-Object Windows.Controls.StackPanel
|
$contentPanel = New-Object Windows.Controls.StackPanel
|
||||||
$contentPanel.Orientation = "Horizontal"
|
$contentPanel.Orientation = "Horizontal"
|
||||||
@@ -71,15 +46,16 @@ function Initialize-InstallAppEntry {
|
|||||||
$fallback = New-Object Windows.Controls.TextBlock
|
$fallback = New-Object Windows.Controls.TextBlock
|
||||||
$fallback.Text = $app.content.TrimStart(".").Substring(0, 1).ToUpper()
|
$fallback.Text = $app.content.TrimStart(".").Substring(0, 1).ToUpper()
|
||||||
$fallback.FontWeight = "Bold"; $fallback.HorizontalAlignment = "Center"; $fallback.VerticalAlignment = "Center"
|
$fallback.FontWeight = "Bold"; $fallback.HorizontalAlignment = "Center"; $fallback.VerticalAlignment = "Center"
|
||||||
if ($app.link) { $fallback.Visibility = "Collapsed" }
|
|
||||||
$fallback.SetResourceReference([Windows.Controls.TextBlock]::FontSizeProperty, "AppEntryFontSize")
|
$fallback.SetResourceReference([Windows.Controls.TextBlock]::FontSizeProperty, "AppEntryFontSize")
|
||||||
$fallback.SetResourceReference([Windows.Controls.TextBlock]::ForegroundProperty, "ToggleButtonOnColor")
|
$fallback.SetResourceReference([Windows.Controls.TextBlock]::ForegroundProperty, "ToggleButtonOnColor")
|
||||||
[void]$icon.Children.Add($fallback)
|
[void]$icon.Children.Add($fallback)
|
||||||
if ($app.link) {
|
if ($app.link) {
|
||||||
|
$fallback.Visibility = "Collapsed"
|
||||||
$logo = New-Object Windows.Controls.Image
|
$logo = New-Object Windows.Controls.Image
|
||||||
$logo.Stretch = [Windows.Media.Stretch]::Uniform
|
$logo.Stretch = [Windows.Media.Stretch]::Uniform
|
||||||
$logo.Source = "https://www.google.com/s2/favicons?sz=64&domain_url=$([uri]::EscapeDataString($app.link))"
|
$logo.Source = "https://www.google.com/s2/favicons?sz=64&domain_url=$([uri]::EscapeDataString($app.link))"
|
||||||
$logo.Add_ImageFailed({ $this.Visibility = "Collapsed"; $this.Parent.Children[0].Visibility = "Visible" })
|
$logo.Add_ImageFailed($handlers.ImageFailed)
|
||||||
|
|
||||||
[void]$icon.Children.Add($logo)
|
[void]$icon.Children.Add($logo)
|
||||||
}
|
}
|
||||||
[void]$contentPanel.Children.Add($icon)
|
[void]$contentPanel.Children.Add($icon)
|
||||||
@@ -90,13 +66,6 @@ function Initialize-InstallAppEntry {
|
|||||||
$appName.Text = $app.content
|
$appName.Text = $app.content
|
||||||
|
|
||||||
# Add FOSS label after the name if FOSS
|
# Add FOSS label after the name if FOSS
|
||||||
if ($app.foss -eq $true) {
|
|
||||||
$fossRun = [System.Windows.Documents.Run]::new(" $([char]0x25CF)")
|
|
||||||
$fossRun.Foreground = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(110, 255, 114))
|
|
||||||
$fossRun.FontSize = 11.5
|
|
||||||
|
|
||||||
[void]$appName.Inlines.Add($fossRun)
|
|
||||||
}
|
|
||||||
[void]$contentPanel.Children.Add($appName)
|
[void]$contentPanel.Children.Add($appName)
|
||||||
$checkBox.Content = $contentPanel
|
$checkBox.Content = $contentPanel
|
||||||
|
|
||||||
@@ -104,7 +73,21 @@ function Initialize-InstallAppEntry {
|
|||||||
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||||
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||||
|
|
||||||
$border.Child = $checkBox
|
# Keep the same layout for every entry so the checkbox handlers can reach the border
|
||||||
|
$entryLayout = New-Object Windows.Controls.Grid
|
||||||
|
[void]$entryLayout.Children.Add($checkBox)
|
||||||
|
|
||||||
|
# Mark FOSS apps with a corner badge, bled into the border padding so it sits on the edge
|
||||||
|
if ($app.foss -eq $true) {
|
||||||
|
$fossBadge = New-WinUtilFossBadge
|
||||||
|
$fossBadge.HorizontalAlignment = "Right"
|
||||||
|
$fossBadge.VerticalAlignment = "Top"
|
||||||
|
$fossBadge.Margin = New-Object Windows.Thickness(0, -4, -6, 0)
|
||||||
|
|
||||||
|
[void]$entryLayout.Children.Add($fossBadge)
|
||||||
|
}
|
||||||
|
|
||||||
|
$border.Child = $entryLayout
|
||||||
if ($sync.selectedApps -contains $appKey) {
|
if ($sync.selectedApps -contains $appKey) {
|
||||||
$checkBox.IsChecked = $true
|
$checkBox.IsChecked = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,17 @@ function Initialize-InstallCategoryAppList {
|
|||||||
$Apps
|
$Apps
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-group apps by category before creating WPF controls.
|
# Pre-group apps by category before creating WPF controls. Lists, because appending to
|
||||||
|
# an array copies it and there are several hundred apps.
|
||||||
$appsByCategory = @{}
|
$appsByCategory = @{}
|
||||||
|
# Indexed, not dynamic member, lookup: the latter goes through the PSObject adapter and
|
||||||
|
# costs about seventy times as much per app.
|
||||||
foreach ($appKey in $Apps.Keys) {
|
foreach ($appKey in $Apps.Keys) {
|
||||||
$category = $Apps.$appKey.Category
|
$category = $Apps[$appKey].Category
|
||||||
if (-not $appsByCategory.ContainsKey($category)) {
|
if (-not $appsByCategory.ContainsKey($category)) {
|
||||||
$appsByCategory[$category] = @()
|
$appsByCategory[$category] = [System.Collections.Generic.List[string]]::new()
|
||||||
}
|
}
|
||||||
$appsByCategory[$category] += $appKey
|
$appsByCategory[$category].Add($appKey)
|
||||||
}
|
}
|
||||||
$sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
|
$sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
|
||||||
|
|
||||||
@@ -62,6 +65,11 @@ function Initialize-InstallCategoryAppList {
|
|||||||
# The WrapPanel is the second child
|
# The WrapPanel is the second child
|
||||||
$wrapPanel = $categoryContainer.Children[1]
|
$wrapPanel = $categoryContainer.Children[1]
|
||||||
|
|
||||||
|
# An explicit click wins over anything filtering expanded automatically
|
||||||
|
if ($sync.AppCategoryAutoExpanded) {
|
||||||
|
$sync.AppCategoryAutoExpanded.Remove(($categoryToggle.Content -replace '^[+-] ', ''))
|
||||||
|
}
|
||||||
|
|
||||||
# Toggle visibility
|
# Toggle visibility
|
||||||
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
|
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
|
||||||
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
function Initialize-WinUtilInstallTabControls {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Wires the Install tab controls that are generated from config rather than declared
|
||||||
|
in XAML
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
The package manager radio buttons and the install action buttons are created by
|
||||||
|
Invoke-WPFUIElements, so they do not exist until the Install tab is built. Setting
|
||||||
|
them up anywhere other than immediately after that build makes the code depend on
|
||||||
|
when the tab happens to be created.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if ($sync.ChocoRadioButton) {
|
||||||
|
$sync.ChocoRadioButton.Add_Checked({
|
||||||
|
$sync.preferences.packagemanager = "Choco"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ($sync.WingetRadioButton) {
|
||||||
|
$sync.WingetRadioButton.Add_Checked({
|
||||||
|
$sync.preferences.packagemanager = "Winget"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($sync.preferences.packagemanager) {
|
||||||
|
"Choco" { if ($sync.ChocoRadioButton) { $sync.ChocoRadioButton.IsChecked = $true }; break }
|
||||||
|
"Winget" { if ($sync.WingetRadioButton) { $sync.WingetRadioButton.IsChecked = $true }; break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PARAM_OFFLINE) {
|
||||||
|
foreach ($name in "WPFInstall", "WPFUninstall", "WPFInstallUpgrade", "WPFGetInstalled") {
|
||||||
|
if ($sync.$name) { $sync.$name.IsEnabled = $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,34 @@
|
|||||||
function Initialize-WinUtilRunspacePool {
|
function Initialize-WinUtilRunspacePool {
|
||||||
if ($sync.runspace -and $sync.runspace.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspacePoolState]::Opened) {
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Opens the shared worker pool that Start-WinUtilJob runs job bodies in
|
||||||
|
#>
|
||||||
|
|
||||||
|
$poolLock = Get-WinUtilRunspacePoolLock
|
||||||
|
[System.Threading.Monitor]::Enter($poolLock)
|
||||||
|
try {
|
||||||
|
if ($sync.runspace -and $sync.runspace.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspacePoolState]::Opened) {
|
||||||
|
return $sync.runspace
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sync.runspace) {
|
||||||
|
# A replacement, not a shutdown
|
||||||
|
Close-WinUtilRunspacePool -Recycle
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine.
|
||||||
|
$maxthreads = [Math]::Max([int]$env:NUMBER_OF_PROCESSORS, 1)
|
||||||
|
|
||||||
|
$sync.runspace = [runspacefactory]::CreateRunspacePool(
|
||||||
|
1, # Minimum thread count
|
||||||
|
$maxthreads, # Maximum thread count
|
||||||
|
(New-WinUtilSessionState), # Initial session state
|
||||||
|
$Host # Machine to create runspaces on
|
||||||
|
)
|
||||||
|
|
||||||
|
$sync.runspace.Open()
|
||||||
return $sync.runspace
|
return $sync.runspace
|
||||||
|
} finally {
|
||||||
|
[System.Threading.Monitor]::Exit($poolLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sync.runspace) {
|
|
||||||
Close-WinUtilRunspacePool
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine.
|
|
||||||
$maxthreads = [Math]::Max([int]$env:NUMBER_OF_PROCESSORS, 1)
|
|
||||||
|
|
||||||
# Create a new session state for parsing variables into our runspace.
|
|
||||||
$hashVars = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'sync', $sync, $null
|
|
||||||
$offlineVar = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_OFFLINE', $PARAM_OFFLINE, $null
|
|
||||||
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
|
|
||||||
|
|
||||||
$initialSessionState.Variables.Add($hashVars)
|
|
||||||
$initialSessionState.Variables.Add($offlineVar)
|
|
||||||
|
|
||||||
# Get every WinUtil/WPF function and add it to the session state.
|
|
||||||
$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
|
|
||||||
$initialSessionState.Commands.Add($functionEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
$sync.runspace = [runspacefactory]::CreateRunspacePool(
|
|
||||||
1, # Minimum thread count
|
|
||||||
$maxthreads, # Maximum thread count
|
|
||||||
$initialSessionState, # Initial session state
|
|
||||||
$Host # Machine to create runspaces on
|
|
||||||
)
|
|
||||||
|
|
||||||
$sync.runspace.Open()
|
|
||||||
return $sync.runspace
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
function Initialize-WinUtilTabContent {
|
function Initialize-WinUtilTabContent {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$TabName
|
[string]$TabName,
|
||||||
|
|
||||||
|
# Build in batches, letting the interface answer in between. Used by the warmup, which
|
||||||
|
# nobody is waiting on. A tab the user just clicked is built in one go.
|
||||||
|
[switch]$Yield
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($null -eq $sync.InitializedTabs) {
|
if ($null -eq $sync.InitializedTabs) {
|
||||||
@@ -12,28 +16,42 @@ function Initialize-WinUtilTabContent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($TabName) {
|
# Claimed before building, not after: a yielding build lets a click through, and that click
|
||||||
"Install" {
|
# would otherwise start building the same tab a second time.
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
|
$sync.InitializedTabs[$TabName] = $true
|
||||||
Initialize-WPFUI -targetGridName "appscategory"
|
|
||||||
|
|
||||||
Initialize-WPFUI -targetGridName "appspanel"
|
try {
|
||||||
}
|
switch ($TabName) {
|
||||||
"Tweaks" {
|
"Install" {
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.tweaks -targetGridName "tweakspanel" -columncount 2
|
Measure-WinUtilStep -Scope "UI" -Name "Install tab: category area" -ScriptBlock {
|
||||||
}
|
Initialize-WPFUI -targetGridName "appscategory"
|
||||||
"Config" {
|
}
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.feature -targetGridName "featurespanel" -columncount 2
|
Measure-WinUtilStep -Scope "UI" -Name "Install tab: app area" -ScriptBlock {
|
||||||
}
|
Initialize-WPFUI -targetGridName "appspanel"
|
||||||
"AppX" {
|
}
|
||||||
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2
|
Initialize-WinUtilInstallTabControls
|
||||||
}
|
}
|
||||||
"Win11ISO" {
|
"Tweaks" {
|
||||||
if ($sync.Form -and $sync.Form.Dispatcher) {
|
Invoke-WPFUIElements -configVariable $sync.configs.tweaks -targetGridName "tweakspanel" -columncount 2 -Yield:$Yield
|
||||||
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
}
|
||||||
|
"Config" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.feature -targetGridName "featurespanel" -columncount 2 -Yield:$Yield
|
||||||
|
}
|
||||||
|
"AppX" {
|
||||||
|
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2 -Yield:$Yield
|
||||||
|
}
|
||||||
|
"Win11ISO" {
|
||||||
|
if (Test-WinUtilUIAlive) {
|
||||||
|
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# Controls built just now start unchecked, so anything already chosen by an import or a
|
||||||
|
# preset has to be applied to them once they exist
|
||||||
|
Reset-WPFCheckBoxes -doToggles $true
|
||||||
|
} catch {
|
||||||
|
# A half built tab must be allowed to rebuild rather than staying empty forever
|
||||||
|
$sync.InitializedTabs[$TabName] = $false
|
||||||
|
throw
|
||||||
}
|
}
|
||||||
|
|
||||||
$sync.InitializedTabs[$TabName] = $true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,30 @@ function Initialize-WinUtilTaskbarOverlayAssets {
|
|||||||
[bool]$IncludeStatusAssets = $true
|
[bool]$IncludeStatusAssets = $true
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($IncludeLogo -and -not $sync["logorender"]) {
|
[System.Threading.Monitor]::Enter($sync.SyncRoot)
|
||||||
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
|
try {
|
||||||
|
if ($null -eq $sync.AssetRenderLock) {
|
||||||
|
$sync.AssetRenderLock = [object]::new()
|
||||||
|
}
|
||||||
|
$assetRenderLock = $sync.AssetRenderLock
|
||||||
|
} finally {
|
||||||
|
[System.Threading.Monitor]::Exit($sync.SyncRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($IncludeStatusAssets -and -not $sync["checkmarkrender"]) {
|
[System.Threading.Monitor]::Enter($assetRenderLock)
|
||||||
$sync["checkmarkrender"] = (Invoke-WinUtilAssets -Type "checkmark" -Size 512 -Render)
|
try {
|
||||||
}
|
if ($IncludeLogo -and -not $sync["logorender"]) {
|
||||||
|
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
|
||||||
|
}
|
||||||
|
|
||||||
if ($IncludeStatusAssets -and -not $sync["warningrender"]) {
|
if ($IncludeStatusAssets -and -not $sync["checkmarkrender"]) {
|
||||||
$sync["warningrender"] = (Invoke-WinUtilAssets -Type "warning" -Size 512 -Render)
|
$sync["checkmarkrender"] = (Invoke-WinUtilAssets -Type "checkmark" -Size 512 -Render)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeStatusAssets -and -not $sync["warningrender"]) {
|
||||||
|
$sync["warningrender"] = (Invoke-WinUtilAssets -Type "warning" -Size 512 -Render)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
[System.Threading.Monitor]::Exit($assetRenderLock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,14 @@ function Install-WinUtilAPPX {
|
|||||||
$manifestPath = ($manifestOutput | Select-Object -Last 1).ToString().Trim()
|
$manifestPath = ($manifestOutput | Select-Object -Last 1).ToString().Trim()
|
||||||
if (-not [string]::IsNullOrWhiteSpace($manifestPath)) {
|
if (-not [string]::IsNullOrWhiteSpace($manifestPath)) {
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Registered local AppX manifest for $Name`: $manifestPath"
|
Write-WinUtilLog -Component "AppX" -Message "Registered local AppX manifest for $Name`: $manifestPath"
|
||||||
return
|
return [pscustomobject]@{
|
||||||
|
Package = $Name
|
||||||
|
Manager = "appx"
|
||||||
|
Action = "Install"
|
||||||
|
ExitCode = 0
|
||||||
|
Outcome = "Succeeded"
|
||||||
|
Detail = "registered local manifest"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +77,12 @@ function Install-WinUtilAPPX {
|
|||||||
if ([string]::IsNullOrWhiteSpace($StoreId)) {
|
if ([string]::IsNullOrWhiteSpace($StoreId)) {
|
||||||
$errorMessage = "Unable to install $Name because no local manifest or Microsoft Store ID is available."
|
$errorMessage = "Unable to install $Name because no local manifest or Microsoft Store ID is available."
|
||||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
|
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
|
||||||
throw $errorMessage
|
$exception = [System.InvalidOperationException]::new($errorMessage)
|
||||||
|
$exception.Data["WinUtilErrorReported"] = $true
|
||||||
|
throw $exception
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-WinUtilLog -Component "AppX" -Message "No usable local manifest found for $Name. Installing Microsoft Store product $StoreId."
|
Write-WinUtilLog -Component "AppX" -Message "No usable local manifest found for $Name. Installing Microsoft Store product $StoreId."
|
||||||
Install-WinUtilWinget
|
$null = Install-WinUtilWinget
|
||||||
Install-WinUtilProgramWinget -Action Install -Programs @("msstore:$StoreId")
|
Install-WinUtilProgramWinget -Action Install -Programs @("msstore:$StoreId")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
function Install-WinUtilChoco {
|
function Install-WinUtilChoco {
|
||||||
if (-not (Get-Command -Name choco)) {
|
<#
|
||||||
Write-Host "Chocolatey is not installed. Installing now..."
|
.SYNOPSIS
|
||||||
$installScript = Invoke-WebRequest -Uri https://community.chocolatey.org/install.ps1 -UseBasicParsing
|
Installs Chocolatey if it is not already present
|
||||||
Invoke-Command -ScriptBlock ([scriptblock]::Create($installScript.Content))
|
#>
|
||||||
|
|
||||||
|
if (Get-Command -Name choco -ErrorAction SilentlyContinue) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "Chocolatey is not installed, installing it now."
|
||||||
|
Step-WinUtilJob -Status "Installing Chocolatey" -State "Indeterminate"
|
||||||
|
|
||||||
|
# Windows PowerShell 5.1 can negotiate a protocol the site refuses, which the official
|
||||||
|
# bootstrap sets explicitly for the same reason
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
|
||||||
|
$installScript = Invoke-WebRequest -Uri https://community.chocolatey.org/install.ps1 -UseBasicParsing -TimeoutSec 60
|
||||||
|
Invoke-Command -ScriptBlock ([scriptblock]::Create($installScript.Content))
|
||||||
|
|
||||||
|
# The installer extends PATH for new processes, which this one is not. Appended rather than
|
||||||
|
# replaced: overwriting drops whatever this process added earlier in the session, and a
|
||||||
|
# later step looking for that tool would no longer find it.
|
||||||
|
$existing = $env:PATH -split ';' | Where-Object { $_ }
|
||||||
|
$persisted = @(
|
||||||
|
[System.Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||||
|
[System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
) -join ';' -split ';' | Where-Object { $_ }
|
||||||
|
|
||||||
|
$missing = $persisted | Where-Object { $existing -notcontains $_ }
|
||||||
|
if ($missing) {
|
||||||
|
$env:PATH = (@($existing) + @($missing)) -join ';'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command -Name choco -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Chocolatey was installed but choco is still not on PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "Chocolatey installed."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,116 @@
|
|||||||
function Install-WinUtilProgramChoco {
|
function Install-WinUtilProgramChoco {
|
||||||
|
<#
|
||||||
|
|
||||||
|
.SYNOPSIS
|
||||||
|
Installs, upgrades or uninstalls packages with Chocolatey and reports each outcome
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
One package per call to choco, so the progress bar moves through the list and a failure
|
||||||
|
names the package that failed rather than the whole batch. Choco's own output goes to the
|
||||||
|
log instead of the console, the way the WinGet path reports.
|
||||||
|
|
||||||
|
.PARAMETER Action
|
||||||
|
Install, Upgrade or Uninstall.
|
||||||
|
|
||||||
|
.PARAMETER Programs
|
||||||
|
The package names. For Upgrade, the single entry "all" upgrades everything.
|
||||||
|
|
||||||
|
.PARAMETER ProgressBase
|
||||||
|
Where this call starts within the job's overall progress bar.
|
||||||
|
|
||||||
|
.PARAMETER ProgressSpan
|
||||||
|
How much of the overall bar these packages account for. Zero reports nothing.
|
||||||
|
|
||||||
|
#>
|
||||||
param (
|
param (
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[ValidateSet("Install", "Uninstall")]
|
[ValidateSet("Install", "Uninstall", "Upgrade")]
|
||||||
[string]$Action,
|
[string]$Action,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string[]]$Programs
|
[string[]]$Programs,
|
||||||
|
|
||||||
|
[int]$ProgressBase = 0,
|
||||||
|
|
||||||
|
[int]$ProgressSpan = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($Action -eq 'Install') {
|
# Chocolatey reports "nothing needed doing" and "it worked, now reboot" through exit codes
|
||||||
$arguments = "install $Programs -y"
|
# rather than as failures
|
||||||
} else {
|
$rebootCodes = @{
|
||||||
$arguments = "uninstall $Programs -y"
|
1641 = "installed, the installer started a restart"
|
||||||
|
3010 = "installed, a restart is needed to finish"
|
||||||
}
|
}
|
||||||
|
$nothingToDo = @{
|
||||||
|
2 = "nothing to do"
|
||||||
|
}
|
||||||
|
$verb = $Action.ToLowerInvariant()
|
||||||
|
$chocoAvailable = $null -ne (Get-Command choco -ErrorAction SilentlyContinue)
|
||||||
|
|
||||||
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s): $($Programs -join ', ')"
|
$packages = @($Programs | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
$process = Start-Process -FilePath choco -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$total = $packages.Count
|
||||||
Write-WinUtilLog -Component "Package" -Message "$Action choco package(s) completed: $($Programs -join ', ') (exit code: $($process.ExitCode))"
|
$index = 0
|
||||||
|
|
||||||
|
foreach ($program in $packages) {
|
||||||
|
$index++
|
||||||
|
if ($ProgressSpan -gt 0 -and $total -gt 0) {
|
||||||
|
$percent = $ProgressBase + [int]((($index - 1) / $total) * $ProgressSpan)
|
||||||
|
Step-WinUtilJob -Status "$Action $program ($index/$total)" -Percent $percent
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "Package" -Message "$Action choco package: $program"
|
||||||
|
|
||||||
|
# --no-progress stops choco redrawing a percentage line that only makes sense on a
|
||||||
|
# console nobody is watching
|
||||||
|
$arguments = @($verb, $program, "-y", "--no-progress")
|
||||||
|
# Each worker runspace has its own global scope. Reset the native-command result there so
|
||||||
|
# command-not-found cannot inherit a successful code from earlier work in the same pool.
|
||||||
|
$global:LASTEXITCODE = $null
|
||||||
|
if (-not $chocoAvailable) {
|
||||||
|
$output = "Chocolatey is not installed or is not available on PATH."
|
||||||
|
$exitCode = -1
|
||||||
|
} else {
|
||||||
|
$output = & choco @arguments 2>&1
|
||||||
|
$exitCode = if ($null -eq $global:LASTEXITCODE) { -1 } else { [int]$global:LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
$outcome = "Succeeded"
|
||||||
|
$detail = "exit code 0"
|
||||||
|
} elseif ($rebootCodes.ContainsKey($exitCode)) {
|
||||||
|
$outcome = "Succeeded"
|
||||||
|
$detail = $rebootCodes[$exitCode]
|
||||||
|
} elseif ($nothingToDo.ContainsKey($exitCode)) {
|
||||||
|
$outcome = "Skipped"
|
||||||
|
$detail = $nothingToDo[$exitCode]
|
||||||
|
} else {
|
||||||
|
$outcome = "Failed"
|
||||||
|
$detail = if ($exitCode -eq -1) { "Chocolatey command did not start" } else { "exit code $exitCode" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = if ($outcome -eq "Failed") { "ERROR" } else { "INFO" }
|
||||||
|
Write-WinUtilLog -Level $level -Component "Package" -Message "$Action choco package $($outcome.ToLowerInvariant()): $program ($detail)"
|
||||||
|
|
||||||
|
if ($outcome -eq "Failed") {
|
||||||
|
# The reason is somewhere in choco's output, and without it the log says only that
|
||||||
|
# a number came back
|
||||||
|
foreach ($line in @($output | Select-Object -Last 15)) {
|
||||||
|
$text = ([string]$line).Trim()
|
||||||
|
if ($text) { Write-WinUtilLog -Level "WARN" -Component "Package" -Detail -Message $text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ProgressSpan -gt 0 -and $total -gt 0) {
|
||||||
|
Step-WinUtilJob -Status "$Action $program ($index/$total)" -Percent ($ProgressBase + [int](($index / $total) * $ProgressSpan))
|
||||||
|
}
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
Package = $program
|
||||||
|
Manager = "choco"
|
||||||
|
Action = $Action
|
||||||
|
ExitCode = $exitCode
|
||||||
|
Outcome = $outcome
|
||||||
|
Detail = $detail
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,116 @@
|
|||||||
Function Install-WinUtilProgramWinget {
|
Function Install-WinUtilProgramWinget {
|
||||||
|
<#
|
||||||
|
|
||||||
|
.SYNOPSIS
|
||||||
|
Installs or uninstalls packages with WinGet and reports the outcome of each one
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Emits one result object per package so the caller can tell what actually happened
|
||||||
|
rather than assuming the run succeeded.
|
||||||
|
|
||||||
|
Runs one winget command per package so a failure names the package that failed rather
|
||||||
|
than the whole batch. Progress moves per package: winget hides its own progress bar once
|
||||||
|
its output is redirected, so there is nothing to report from inside a single install.
|
||||||
|
|
||||||
|
#>
|
||||||
param (
|
param (
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[ValidateSet("Install", "Uninstall")]
|
[ValidateSet("Install", "Uninstall", "Upgrade")]
|
||||||
[string]$Action,
|
[string]$Action,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string[]]$Programs
|
[string[]]$Programs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_ACTION_PROHIBITED. WinGet refuses to act on a package
|
||||||
|
# that was installed in user scope while it is running elevated, and WinUtil is always
|
||||||
|
# elevated, so every per-user app answers this and nothing happens.
|
||||||
|
$adminContextProhibited = -1978335107
|
||||||
|
|
||||||
|
# WinGet reports "there was nothing to do" through the exit code rather than as success
|
||||||
|
$nothingToDo = @{
|
||||||
|
-1978335135 = "already installed"
|
||||||
|
-1978335189 = "no applicable update"
|
||||||
|
}
|
||||||
|
# The installer worked and wants a restart to finish. Windows reports that as its own exit
|
||||||
|
# code rather than as zero, and treating it as a failure marks working installs as broken.
|
||||||
|
$rebootExitCodes = @{
|
||||||
|
3010 = "installed, a restart is needed to finish"
|
||||||
|
1641 = "installed, the installer started a restart"
|
||||||
|
# WinGet's own equivalents. -1978334966 is deliberately absent: it means a reboot is
|
||||||
|
# required before the install can proceed, which is not a completed install.
|
||||||
|
-1978334967 = "installed, a restart is needed to finish"
|
||||||
|
-1978334965 = "installed, the installer started a restart"
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($program in $Programs) {
|
foreach ($program in $Programs) {
|
||||||
if ([string]::IsNullOrWhiteSpace($program) -or $program -eq "na") {
|
if ([string]::IsNullOrWhiteSpace($program) -or $program -eq "na") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$source = "winget"
|
$upgradeAll = $Action -eq "Upgrade" -and $program -eq "all"
|
||||||
if ($program.StartsWith("msstore:", [System.StringComparison]::OrdinalIgnoreCase)) {
|
$source = if ($upgradeAll) { "all configured sources" } else { "winget" }
|
||||||
|
if (-not $upgradeAll -and $program.StartsWith("msstore:", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
$source = "msstore"
|
$source = "msstore"
|
||||||
$program = $program.Substring("msstore:".Length)
|
$program = $program.Substring("msstore:".Length)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Action -eq 'Install') {
|
Write-WinUtilLog -Component "Package" -Message "$Action winget package: $program (source: $source)"
|
||||||
$arguments = @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent")
|
|
||||||
} else {
|
$outcome = "Failed"
|
||||||
$arguments = @("uninstall", "--id", $program, "--source", $source, "--silent")
|
$detail = "no result"
|
||||||
|
$exitCode = -1
|
||||||
|
|
||||||
|
$arguments = switch ($Action) {
|
||||||
|
"Uninstall" { @("uninstall", "--id", $program, "--source", $source, "--silent") }
|
||||||
|
# --include-unknown because the scan that found these ran with it: without it winget
|
||||||
|
# refuses every package whose installed version it could not read
|
||||||
|
"Upgrade" {
|
||||||
|
if ($upgradeAll) {
|
||||||
|
@("upgrade", "--all", "--accept-package-agreements", "--accept-source-agreements", "--include-unknown", "--silent")
|
||||||
|
} else {
|
||||||
|
@("upgrade", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--include-unknown", "--silent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default { @("install", "--id", $program, "--accept-package-agreements", "--accept-source-agreements", "--source", $source, "--silent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-WinUtilLog -Component "Package" -Message "$Action winget package: $program (source: $source)"
|
|
||||||
$process = Start-Process -FilePath winget -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath winget -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
Write-WinUtilLog -Component "Package" -Message "$Action winget package completed: $program (exit code: $($process.ExitCode))"
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
$outcome = "Succeeded"
|
||||||
|
$detail = "exit code 0"
|
||||||
|
} elseif ($rebootExitCodes.ContainsKey($exitCode)) {
|
||||||
|
$outcome = "Succeeded"
|
||||||
|
$detail = $rebootExitCodes[$exitCode]
|
||||||
|
} elseif ($nothingToDo.ContainsKey($exitCode)) {
|
||||||
|
$outcome = "Skipped"
|
||||||
|
$detail = $nothingToDo[$exitCode]
|
||||||
|
} elseif ($exitCode -eq $adminContextProhibited) {
|
||||||
|
$outcome = "Skipped"
|
||||||
|
$detail = switch ($Action) {
|
||||||
|
"Install" { "already installed for the current user; elevated WinUtil cannot update it" }
|
||||||
|
"Upgrade" { "not upgraded; installed for the current user and elevated WinUtil cannot modify it" }
|
||||||
|
"Uninstall" { "remains installed for the current user; elevated WinUtil cannot uninstall it" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$outcome = "Failed"
|
||||||
|
# The client module reports the same failure as a bare HRESULT, so the hex form and
|
||||||
|
# Microsoft's own list serve both paths
|
||||||
|
$detail = "WinGet reported 0x{0:X8}. See https://learn.microsoft.com/windows/package-manager/winget/returnCodes" -f $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = if ($outcome -eq "Failed") { "ERROR" } else { "INFO" }
|
||||||
|
Write-WinUtilLog -Level $level -Component "Package" -Message "$Action winget package $($outcome.ToLowerInvariant()): $program ($detail)"
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
Package = $program
|
||||||
|
Manager = "winget"
|
||||||
|
Action = $Action
|
||||||
|
ExitCode = $exitCode
|
||||||
|
Outcome = $outcome
|
||||||
|
Detail = $detail
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,21 @@ function Install-WinUtilWinget {
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
installs winGet if needed
|
installs winGet if needed
|
||||||
#>
|
#>
|
||||||
if ((Test-WinUtilPackageManager -winget) -eq "installed") {
|
param(
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
# The repair action needs Repair-WinGetPackageManager to run even when winget is detected,
|
||||||
|
# which is the case a broken installation presents
|
||||||
|
if (-not $Force -and (Test-WinUtilPackageManager -winget) -eq "installed") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "WinGet is not installed. Installing now..." -ForegroundColor Red
|
if ($Force) {
|
||||||
|
Write-Host "Repairing the WinGet installation..." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host "WinGet is not installed. Installing now..." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
Install-PackageProvider -Name NuGet -Force
|
Install-PackageProvider -Name NuGet -Force
|
||||||
Install-Module -Name Microsoft.WinGet.Client -Force
|
Install-Module -Name Microsoft.WinGet.Client -Force
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
function Invoke-WinUtilAppCategoryChip {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Handles a click on an Install tab category chip
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
The chip carries its category in Tag, so every chip shares this handler. Holding ctrl
|
||||||
|
adds the category to the current selection instead of replacing it.
|
||||||
|
|
||||||
|
.PARAMETER Chip
|
||||||
|
The chip that was clicked
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Chip
|
||||||
|
)
|
||||||
|
|
||||||
|
$ctrlDown = [bool]([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)
|
||||||
|
Set-WinUtilAppCategoryFilter -Category $Chip.Tag -Additive:$ctrlDown
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ function Invoke-WinUtilAssets {
|
|||||||
|
|
||||||
if ($render -and $null -ne $sync) {
|
if ($render -and $null -ne $sync) {
|
||||||
if ($null -eq $sync.RenderedAssetCache) {
|
if ($null -eq $sync.RenderedAssetCache) {
|
||||||
$sync.RenderedAssetCache = @{}
|
$sync.RenderedAssetCache = [Hashtable]::Synchronized(@{})
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = "$(([string]$type).ToLowerInvariant())|$Size"
|
$cacheKey = "$(([string]$type).ToLowerInvariant())|$Size"
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
function Invoke-WinUtilCloseRequest {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Asks what to do about work that is still running when the window is closed
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
A half finished install or tweak run is not ended without asking. Either it finishes
|
||||||
|
without the window, reporting to the console and then exiting, or it is stopped and
|
||||||
|
everything closes now.
|
||||||
|
|
||||||
|
.PARAMETER RunningJob
|
||||||
|
The name of the job in flight, so the question names what is at stake.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RunningJob
|
||||||
|
)
|
||||||
|
|
||||||
|
# The question carries the meaning rather than naming buttons: Windows labels them in its own
|
||||||
|
# language, so "Yes" in the text would not match a button reading "Ja".
|
||||||
|
$answer = Show-WinUtilMessage -Button "YesNoCancel" -Icon "Warning" -Title "$RunningJob is still running" -Message @"
|
||||||
|
$RunningJob has not finished yet.
|
||||||
|
|
||||||
|
Close the window and let it finish in the console?
|
||||||
|
|
||||||
|
WinUtil will exit on its own once it is done. If you do not, it will be
|
||||||
|
stopped and everything closes now. Cancel keeps WinUtil open.
|
||||||
|
"@
|
||||||
|
|
||||||
|
switch ("$answer") {
|
||||||
|
"Yes" {
|
||||||
|
Write-WinUtilLog -Component "UI" -Message "Close requested: closing the window, $RunningJob continues in the console."
|
||||||
|
$sync.FinishInConsole = $true
|
||||||
|
$sync.ForceClose = $true
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WinUtil's window is closed. $RunningJob is still running here, and this window will close when it finishes." -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Posted rather than closed from inside the handler that is already unwinding
|
||||||
|
Request-WinUtilWindowClose
|
||||||
|
}
|
||||||
|
"No" {
|
||||||
|
Write-WinUtilLog -Component "UI" -Message "Close requested: stopping $RunningJob."
|
||||||
|
Step-WinUtilJob -Status "Stopping $RunningJob" -State "Indeterminate"
|
||||||
|
$sync.ForceClose = $true
|
||||||
|
|
||||||
|
# Close the window first. The main thread owns pool shutdown after ShowDialog
|
||||||
|
# returns, so the worker can finish its UI-dispatching finally block before the UI
|
||||||
|
# runspace is disposed. Waiting for it here would deadlock the dispatcher.
|
||||||
|
Request-WinUtilWindowClose
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-WinUtilLog -Component "UI" -Message "Close cancelled, $RunningJob is still running."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Request-WinUtilWindowClose {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Closes the window from outside the handler that is currently cancelling the close
|
||||||
|
|
||||||
|
#>
|
||||||
|
if (-not (Test-WinUtilUIAlive)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
|
||||||
|
$sync.Form.Close()
|
||||||
|
}) | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-WinUtilRemainingWork {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Waits for work that outlived the window, reporting to the console
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Runs on the main thread once the interface has gone. The job is still on the worker
|
||||||
|
pool and keeps logging, so this only waits and keeps the wait visible.
|
||||||
|
|
||||||
|
.PARAMETER TimeoutMinutes
|
||||||
|
Upper bound, so a worker that never returns cannot keep the process alive.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
# Double rather than int: an int silently truncates a fractional value to zero, which
|
||||||
|
# turns the bound into "do not wait at all"
|
||||||
|
[double]$TimeoutMinutes = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $sync.FinishInConsole -or -not $sync.ActiveJob) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $sync.ActiveJob
|
||||||
|
Write-WinUtilLog -Component "UI" -Message "Window closed, waiting for $job to finish."
|
||||||
|
Write-Host "Waiting for $job to finish..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$clock = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
while ($sync.ActiveJob -and $clock.Elapsed.TotalMinutes -lt $TimeoutMinutes) {
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
}
|
||||||
|
|
||||||
|
# The job's last progress line is still open, so anything after it needs a fresh line
|
||||||
|
Complete-WinUtilConsoleProgress
|
||||||
|
|
||||||
|
if ($sync.ActiveJob) {
|
||||||
|
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "$job did not finish within $TimeoutMinutes minutes, exiting anyway."
|
||||||
|
Write-Host "$job is taking longer than $TimeoutMinutes minutes. Exiting." -ForegroundColor Yellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "UI" -Message "$job finished after the window closed, in $([int]$clock.Elapsed.TotalSeconds)s."
|
||||||
|
Write-Host "$job finished. Closing." -ForegroundColor Green
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
#>
|
#>
|
||||||
|
|
||||||
param(
|
param(
|
||||||
$CheckBox
|
$CheckBox,
|
||||||
|
[switch]$BypassToggleStatusCache,
|
||||||
|
[switch]$StopOnReadError
|
||||||
)
|
)
|
||||||
if ($CheckBox -eq "choco") {
|
if ($CheckBox -eq "choco") {
|
||||||
$apps = (choco list | Select-String -Pattern "^\S+").Matches.Value
|
$apps = (choco list | Select-String -Pattern "^\S+").Matches.Value
|
||||||
@@ -52,6 +54,7 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
if ($CheckBox -eq "tweaks") {
|
if ($CheckBox -eq "tweaks") {
|
||||||
|
|
||||||
if (!(Test-Path 'HKU:\')) {$null = (New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS)}
|
if (!(Test-Path 'HKU:\')) {$null = (New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS)}
|
||||||
|
$readErrorAction = if ($StopOnReadError) { "Stop" } else { "SilentlyContinue" }
|
||||||
|
|
||||||
$sync.configs.tweaks | Get-Member -MemberType NoteProperty | ForEach-Object {
|
$sync.configs.tweaks | Get-Member -MemberType NoteProperty | ForEach-Object {
|
||||||
|
|
||||||
@@ -61,11 +64,13 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
$serviceKeys = $entry.service
|
$serviceKeys = $entry.service
|
||||||
$entryType = $entry.Type
|
$entryType = $entry.Type
|
||||||
|
|
||||||
if ($registryKeys -or $serviceKeys) {
|
if (($registryKeys -or $serviceKeys) -and $entryType -ne "Combobox") {
|
||||||
$Values = @()
|
$Values = @()
|
||||||
|
|
||||||
if ($entryType -eq "Toggle") {
|
if ($entryType -eq "Toggle") {
|
||||||
if (-not (Get-WinUtilToggleStatus $Config)) {
|
if (-not (Get-WinUtilToggleStatus $Config `
|
||||||
|
-BypassCache:$BypassToggleStatusCache `
|
||||||
|
-StopOnReadError:$StopOnReadError)) {
|
||||||
$values += $False
|
$values += $False
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -77,8 +82,12 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
$registryTotal++
|
$registryTotal++
|
||||||
$regstate = $null
|
$regstate = $null
|
||||||
|
|
||||||
if (Test-Path $tweak.Path) {
|
if (Test-Path $tweak.Path -ErrorAction $readErrorAction) {
|
||||||
$regstate = Get-ItemProperty -Name $tweak.Name -Path $tweak.Path -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $($tweak.Name)
|
if ($StopOnReadError) {
|
||||||
|
$regstate = (Get-ItemProperty -Path $tweak.Path -ErrorAction Stop).$($tweak.Name)
|
||||||
|
} else {
|
||||||
|
$regstate = Get-ItemProperty -Name $tweak.Name -Path $tweak.Path -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $($tweak.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($null -eq $regstate) {
|
if ($null -eq $regstate) {
|
||||||
@@ -108,7 +117,17 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
|
|
||||||
Foreach ($tweaks in $serviceKeys) {
|
Foreach ($tweaks in $serviceKeys) {
|
||||||
Foreach ($tweak in $tweaks) {
|
Foreach ($tweak in $tweaks) {
|
||||||
$Service = Get-Service -Name $tweak.Name
|
try {
|
||||||
|
$Service = Get-Service -Name $tweak.Name -ErrorAction $readErrorAction
|
||||||
|
} catch {
|
||||||
|
if ($StopOnReadError -and $_.FullyQualifiedErrorId -like "NoServiceFoundForGivenName*") {
|
||||||
|
# A removed optional service means this tweak is not applied; it does
|
||||||
|
# not make the registry and service state for every other tweak unknown.
|
||||||
|
$values += $False
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
|
||||||
if ($Service) {
|
if ($Service) {
|
||||||
$actualValue = $Service.StartType
|
$actualValue = $Service.StartType
|
||||||
@@ -116,6 +135,8 @@ Function Invoke-WinUtilCurrentSystem {
|
|||||||
if ($expectedValue -ne $actualValue) {
|
if ($expectedValue -ne $actualValue) {
|
||||||
$values += $False
|
$values += $False
|
||||||
}
|
}
|
||||||
|
} elseif ($StopOnReadError) {
|
||||||
|
$values += $False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||