Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@

# Seanh1917 (ANGRYxScotsmans) is the docs guy
docs/ @ChrisTitusTech @seanh1995
tools/devdocs-generator.* @ChrisTitusTech @seanh1995
tools/devdocs-generator.* @ChrisTitusTech @seanh1995

# Title screen generation
tools/title-screen/ @ChrisTitusTech @mewclouds
207 changes: 207 additions & 0 deletions .github/workflows/generate-title-screen.yaml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
ref: main
Comment thread
coderabbitai[bot] marked this conversation as resolved.
persist-credentials: false

- name: Install uv and Python
uses: astral-sh/setup-uv@v9.0.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
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ testResults.xml
# general software/os specific
desktop.ini
.DS_Store
__pycache__/
*.pyc
.venv/

.vscode/
.idea/
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Given the current wave of npm/pnpm/yarn supply-chain worms (malicious postinstal
- Treat any `postinstall`/`preinstall` lifecycle script in a new dependency as worth flagging to the user before installing — summarize what it does.
- 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.
- 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).
- This Docker requirement is specific to `docs/`. The rest of the repo is PowerShell (`Compile.ps1`, Pester, Script Analyzer) and runs directly on the host per Section 1.
- 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.

## 3. Source Of Truth

Expand Down Expand Up @@ -127,6 +127,7 @@ If a check cannot be run, say exactly why and what residual risk remains. See SP

- Treat local `winutil.ps1` changes as disposable compile output.
- 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.
- Before finishing, check `git status --short` and separate your changes from pre-existing user changes.
- Do not revert user changes unless explicitly asked.
Expand Down
3 changes: 3 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ WinUtil is a Windows PowerShell utility with a WPF interface. The repository is
- `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.

Expand Down Expand Up @@ -97,6 +98,7 @@ Because the final script is concatenated, code cannot rely on runtime module imp
- `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
Expand All @@ -106,6 +108,7 @@ Because the final script is concatenated, code cannot rely on runtime module imp
- 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.
- GitHub Actions also runs PowerShell Script Analyzer with `lint/PSScriptAnalyser.ps1` on every push.
- 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.

## Release Artifact

Expand Down
1 change: 1 addition & 0 deletions tools/title-screen/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
75 changes: 75 additions & 0 deletions tools/title-screen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# WinUtil title screen

This tool generates the Light and Dark composite used as WinUtil's title screen
in the repository README and documentation site. It opens the Tweaks tab,
captures both themes, and combines them into one PNG. The two raw captures are
temporary and are removed when the command finishes.

## Requirements

- Windows with an interactive desktop
- [uv](https://docs.astral.sh/uv/)
- WinUtil compiled and running
- An elevated PowerShell terminal

Run the commands below from `tools/title-screen`.

## Generate and review a test image

Start WinUtil from the repository root:

```powershell
.\Compile.ps1 -Run
```

With WinUtil still open, return to this directory in an elevated terminal and
generate a test image:

```powershell
uv run --locked python automate_title_screen.py --output "$env:TEMP\winutil-title-screen.png"
```

Open the resulting PNG and check that:

- the Tweaks tab is shown,
- the Light theme is on the upper-left side of the diagonal,
- the Dark theme is on the lower-right side, and
- no desktop background or other windows are visible.

The automation works whether WinUtil starts in Light or Dark mode. It leaves the
window on the Tweaks tab in Light mode.

## Automation

The title screen is updated through a manually triggered GitHub Actions workflow.
When the generated image changes, the workflow opens or updates a pull request
for review. It does not merge the pull request automatically.

Failed runs upload the capture log, UI Automation inspection, and available image
as diagnostic artifacts for 14 days.

## Tests

The tests cover theme detection and composite image generation without opening
WinUtil:

```powershell
uv run --locked python -m unittest discover
```

## Troubleshooting

If WinUtil cannot be found, make sure the compiled WPF window is open and that
the terminal is elevated. The script deliberately ignores editors, terminals,
and browser windows that merely contain "WinUtil" in their title.

If a tab or theme control cannot be found, capture the UI Automation tree while
WinUtil is open:

```powershell
uv run --locked python inspect_winutil.py "$env:TEMP\winutil-inspect.txt"
```

The inspector opens the theme menu before recording its controls. Attach the
text file when reporting a failure. It contains window and control metadata, not
the generated screenshots.
Comment thread
mewclouds marked this conversation as resolved.
Loading