Automate WinUtil title screen generation - #4965
Conversation
Add local automation for capturing WinUtil in Light and Dark themes and generating the composite title-screen image.
Add a manual Windows workflow that compiles WinUtil, generates the Light and Dark composite, and opens an image-only pull request.
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a Windows title-screen toolchain. It captures WinUtil in Dark and Light themes, validates UI state, creates a composite PNG, provides diagnostics and tests, and adds a manual workflow that creates image-only pull requests. ChangesTitle-screen generation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant WinUtil
participant automate_title_screen
participant capture_winutil
participant create_composite
participant PullRequest
GitHubActions->>WinUtil: compile and launch from main
GitHubActions->>automate_title_screen: run generation
automate_title_screen->>WinUtil: select Tweaks and themes
automate_title_screen->>capture_winutil: capture Dark and Light states
capture_winutil-->>automate_title_screen: return PNG captures
automate_title_screen->>create_composite: create composite PNG
GitHubActions->>PullRequest: commit generated image and open or update PR
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (10)
.github/workflows/generate-title-screen.yaml (2)
10-12: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
cancel-in-progress: truecan cancel a run during pull request creation.A second manual dispatch cancels the first run. If the cancellation lands between the branch push and the pull request creation, the
title-screen-updatebranch remains without a pull request. The next run recovers becausecreate-pull-requestreuses the branch, so the impact is limited to a stray branch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/generate-title-screen.yaml around lines 10 - 12, Update the workflow concurrency configuration for winutil-title-screen by disabling cancel-in-progress so an active run can complete branch pushing and pull request creation. Preserve the existing concurrency group.
39-52: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSet
$ErrorActionPreferencetoStopin the nested PowerShell command.A missing
Set-DisplayResolutioncmdlet can produce exit code0, so$LASTEXITCODEdoes not report the failure. The laterGetSystemMetricscheck still detects a resolution mismatch; this change provides the correct failure signal and error context.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/generate-title-screen.yaml around lines 39 - 52, Update the nested PowerShell command in the “Set display resolution” step to set $ErrorActionPreference to "Stop" before invoking Set-DisplayResolution, ensuring missing-cmdlet or other PowerShell errors terminate the command with failure details while preserving the existing $LASTEXITCODE check.tools/title-screen/test_create_composite.py (2)
13-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a case for the mismatched-dimension guard.
create_compositeraisesValueErrorwhen the Dark and Light sizes differ. That branch has no coverage.💚 Proposed additional test
`@staticmethod` def _create_capture(path: Path, content_color: tuple[int, int, int, int]): image = Image.new("RGBA", (70, 46), (0, 0, 0, 255)) draw = ImageDraw.Draw(image) draw.rectangle((3, 3, 66, 42), fill=content_color) image.save(path, "PNG") + + def test_rejects_mismatched_dimensions(self): + with tempfile.TemporaryDirectory(prefix="winutil-composite-test-") as temp_dir: + work_dir = Path(temp_dir) + dark_path = work_dir / "dark.png" + light_path = work_dir / "light.png" + self._create_capture(dark_path, (32, 34, 36, 255)) + Image.new("RGBA", (60, 40), (235, 237, 239, 255)).save(light_path, "PNG") + + with self.assertRaises(ValueError): + create_composite(dark_path, light_path, work_dir / "out.png")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/test_create_composite.py` around lines 13 - 41, Add a test method alongside test_creates_only_requested_composite that creates Dark and Light captures with different dimensions, asserts create_composite raises ValueError, and verifies the mismatched-dimension guard is covered without requiring an output file.
9-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBoth test modules use flat imports and only run from
tools/title-screen. Neither file belongs to a package, and neither adds the module directory tosys.path. Test discovery from the repository root fails withModuleNotFoundError. Declare the test path intools/title-screen/pyproject.toml, or document the required working directory intools/title-screen/README.md.
tools/title-screen/test_create_composite.py#L9-L9: makefrom create_composite import BORDER_COLOR, create_compositeresolvable from the repository root.tools/title-screen/test_automate_title_screen.py#L7-L7: makefrom automate_title_screen import is_dark_moderesolvable from the repository root. This import also loadspywinauto, so the module fails to import on non-Windows machines even though the test itself needs no GUI. Consider importingis_dark_modefrom a Windows-free helper module.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/test_create_composite.py` at line 9, Make both title-screen test imports work when tests run from the repository root: update tools/title-screen/pyproject.toml or document the required working directory in tools/title-screen/README.md for test_create_composite.py line 9, and apply the same resolution to test_automate_title_screen.py line 7. Also move or reuse is_dark_mode from automate_title_screen through a Windows-free helper so the test does not import pywinauto on non-Windows systems.tools/title-screen/test_automate_title_screen.py (1)
11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd coverage for
set_themeinput validation andcapture_thememismatch detection.
set_themerejects unsupported theme names, andcapture_themeraises when the capture does not matchexpected_dark. Neither needs a GUI:set_themefails before connecting, andcapture_themeaccepts an injected capture function only if you refactor it. Testing theValueErrorpath is the cheap win.💚 Proposed additional test
self.assertTrue(is_dark_mode(dark_image)) self.assertFalse(is_dark_mode(light_image)) + + def test_rejects_unsupported_theme_name(self): + with self.assertRaises(ValueError): + set_theme(0, "Sepia")Import
set_themealongsideis_dark_modefor this test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/test_automate_title_screen.py` around lines 11 - 16, Extend test_classifies_dark_and_light_captures to import set_theme and assert that unsupported theme names raise ValueError, without requiring GUI setup. Prioritize this validation-path coverage; capture_theme mismatch coverage is optional only if its capture dependency is refactored to be injectable.tools/title-screen/create_composite.py (1)
35-89: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider
getbbox()on a thresholded mask instead of per-pixel Python scans.The four generator scans read pixels one at a time through
Image.load(). For a 1920x1080 capture with a wide shadow, this runs many Python-level iterations. Pillow can do the same work in C:♻️ Alternative bounds detection
- width, height = image.size - pixels = image.load() - - left = next(...) + width, height = image.size + content_mask = image.convert("L").point( + lambda value: 255 if value > BRIGHTNESS_THRESHOLD else 0 + ) + bbox = content_mask.getbbox() + if bbox is None: + left, top, right, bottom = 0, 0, width - 1, height - 1 + else: + left, top, right, bottom = bbox[0], bbox[1], bbox[2] - 1, bbox[3] - 1Note one behavior difference: the luminance conversion weights channels, while
_contains_contenttests each channel separately. If per-channel detection matters for colored edge pixels, keep the current logic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/create_composite.py` around lines 35 - 89, Optimize _find_content_bounds by creating a thresholded Pillow mask and using its getbbox() to detect content bounds, replacing the per-pixel Image.load() generator scans while preserving the existing fallback, margin warning, and returned coordinate format. Verify that luminance-based thresholding does not lose colored edge pixels; if _contains_content’s per-channel behavior is required, retain the current detection logic instead.tools/title-screen/capture_winutil.py (2)
30-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNarrow the DPI-awareness fallback and record the failure reason.
Both handlers catch
Exceptionand the inner handler discards it. Ruff flags this asBLE001andS110.SetProcessDpiAwarenessContextandSetProcessDpiAwarenessfail withOSError, and a missing export raisesAttributeError. A silent failure here produces virtualized coordinates and a mis-sized capture, so the reason should stay visible.♻️ Proposed narrowing
-try: - ctypes.windll.user32.SetProcessDpiAwarenessContext(-4) -except Exception: - try: - ctypes.windll.shcore.SetProcessDpiAwareness(2) - except Exception: - pass +try: + ctypes.windll.user32.SetProcessDpiAwarenessContext(-4) +except (AttributeError, OSError): + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) + except (AttributeError, OSError) as exc: + print(f"Warning: could not enable per-monitor DPI awareness: {exc}")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/capture_winutil.py` around lines 30 - 36, Update the DPI-awareness setup around SetProcessDpiAwarenessContext and SetProcessDpiAwareness to catch only OSError and AttributeError, and retain the failure reason by logging or otherwise reporting the caught exceptions instead of silently passing. Preserve the fallback from the context API to the shcore API while ensuring failures from both attempts remain visible.Source: Linters/SAST tools
344-358: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePreserve the
PrintWindowerror codes.Load
user32withctypes.WinDLL("user32", use_last_error=True)and replace bothkernel32.GetLastError()calls withctypes.get_last_error()immediately after each failed call. Otherwise, Python orctypesactivity can overwrite the live Windows error state and make the diagnostic codes unreliable. Capture behavior is unchanged.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/capture_winutil.py` around lines 344 - 358, Load user32 with ctypes.WinDLL("user32", use_last_error=True), then update both failed-call branches around PrintWindow to capture errors via ctypes.get_last_error() immediately after each failed invocation instead of kernel32.GetLastError(). Preserve the existing retry and RuntimeError behavior.tools/title-screen/inspect_winutil.py (1)
28-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMark the intentional blind excepts so Ruff stays quiet.
A diagnostics dumper must survive stale UIA elements, so catching
Exceptionhere is correct. Ruff still reportsBLE001on lines 37, 43, 60, and 123. Add# noqa: BLE001with a short reason, or configure a per-file ignore in the project Ruff settings.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/inspect_winutil.py` around lines 28 - 45, Mark the intentional broad Exception handlers in inspect_winutil.py with Ruff BLE001 suppression and a brief reason, covering the handlers at the element inspection, child enumeration, and other reported locations (lines 37, 43, 60, and 123). Alternatively, add a narrowly scoped per-file BLE001 ignore in the Ruff configuration for this diagnostics dumper.Source: Linters/SAST tools
tools/title-screen/automate_title_screen.py (1)
40-56: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueGuard the center-band crop for short captures.
If
heightis below 20,height // 2 - 10is negative. Pillow then pads the crop with black pixels and biases the result toward Dark. A minimum-size check keeps the classification honest.🛡️ Proposed guard
width, height = image.size + if width < 4 or height < 20: + raise ValueError(f"Capture is too small to classify: {width}x{height}") sample = image.crop(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/automate_title_screen.py` around lines 40 - 56, Update is_dark_mode to guard against captures with height below the 20-pixel center-band requirement before cropping, returning the appropriate non-dark classification for undersized images; preserve the existing crop and brightness-threshold logic for valid heights.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/generate-title-screen.yaml:
- Around line 27-30: Update the actions/checkout step in the workflow to set
persist-credentials to false, preventing the default job token from being stored
in the repository’s Git configuration while preserving the existing main branch
checkout.
- Around line 6-8: Reduce the workflow-level permissions block to only contents:
read, removing pull-requests: write and contents: write because the pull request
step authenticates with secrets.AUTO_MERGE rather than GITHUB_TOKEN.
- Line 28: Update the actions/checkout step to reference its full immutable
commit SHA instead of the mutable v7 tag, preserving the existing checkout
behavior and permissions.
In `@tools/title-screen/automate_title_screen.py`:
- Around line 205-232: Update the capture flow around capture_theme so
get_window_capture_size(hwnd) is called immediately before both the dark and
light captures, passing the freshly read dimensions to each capture. Preserve
the existing theme-selection and composite behavior; the dimensions returned by
maximize_window may remain only for startup logging or be removed if no longer
needed.
In `@tools/title-screen/pyproject.toml`:
- Around line 1-12: Update the repository operating instructions in AGENTS.md to
acknowledge the Python/uv project under tools/title-screen instead of describing
the rest of the repository as exclusively PowerShell. Document the tool’s
supported uv/Python commands and verification path so agents run its checks when
modifying the title-screen project.
In `@tools/title-screen/README.md`:
- Around line 1-75: Shorten the tools/title-screen README to a brief high-level
entry point describing the tool’s purpose and linking to a hand-written
documentation page under docs. Move the requirements, generation procedure,
automation details, tests, and troubleshooting guidance into that docs page, or
document an intentional tool-local README exception in AGENTS.md if retaining
the detailed README.
---
Nitpick comments:
In @.github/workflows/generate-title-screen.yaml:
- Around line 10-12: Update the workflow concurrency configuration for
winutil-title-screen by disabling cancel-in-progress so an active run can
complete branch pushing and pull request creation. Preserve the existing
concurrency group.
- Around line 39-52: Update the nested PowerShell command in the “Set display
resolution” step to set $ErrorActionPreference to "Stop" before invoking
Set-DisplayResolution, ensuring missing-cmdlet or other PowerShell errors
terminate the command with failure details while preserving the existing
$LASTEXITCODE check.
In `@tools/title-screen/automate_title_screen.py`:
- Around line 40-56: Update is_dark_mode to guard against captures with height
below the 20-pixel center-band requirement before cropping, returning the
appropriate non-dark classification for undersized images; preserve the existing
crop and brightness-threshold logic for valid heights.
In `@tools/title-screen/capture_winutil.py`:
- Around line 30-36: Update the DPI-awareness setup around
SetProcessDpiAwarenessContext and SetProcessDpiAwareness to catch only OSError
and AttributeError, and retain the failure reason by logging or otherwise
reporting the caught exceptions instead of silently passing. Preserve the
fallback from the context API to the shcore API while ensuring failures from
both attempts remain visible.
- Around line 344-358: Load user32 with ctypes.WinDLL("user32",
use_last_error=True), then update both failed-call branches around PrintWindow
to capture errors via ctypes.get_last_error() immediately after each failed
invocation instead of kernel32.GetLastError(). Preserve the existing retry and
RuntimeError behavior.
In `@tools/title-screen/create_composite.py`:
- Around line 35-89: Optimize _find_content_bounds by creating a thresholded
Pillow mask and using its getbbox() to detect content bounds, replacing the
per-pixel Image.load() generator scans while preserving the existing fallback,
margin warning, and returned coordinate format. Verify that luminance-based
thresholding does not lose colored edge pixels; if _contains_content’s
per-channel behavior is required, retain the current detection logic instead.
In `@tools/title-screen/inspect_winutil.py`:
- Around line 28-45: Mark the intentional broad Exception handlers in
inspect_winutil.py with Ruff BLE001 suppression and a brief reason, covering the
handlers at the element inspection, child enumeration, and other reported
locations (lines 37, 43, 60, and 123). Alternatively, add a narrowly scoped
per-file BLE001 ignore in the Ruff configuration for this diagnostics dumper.
In `@tools/title-screen/test_automate_title_screen.py`:
- Around line 11-16: Extend test_classifies_dark_and_light_captures to import
set_theme and assert that unsupported theme names raise ValueError, without
requiring GUI setup. Prioritize this validation-path coverage; capture_theme
mismatch coverage is optional only if its capture dependency is refactored to be
injectable.
In `@tools/title-screen/test_create_composite.py`:
- Around line 13-41: Add a test method alongside
test_creates_only_requested_composite that creates Dark and Light captures with
different dimensions, asserts create_composite raises ValueError, and verifies
the mismatched-dimension guard is covered without requiring an output file.
- Line 9: Make both title-screen test imports work when tests run from the
repository root: update tools/title-screen/pyproject.toml or document the
required working directory in tools/title-screen/README.md for
test_create_composite.py line 9, and apply the same resolution to
test_automate_title_screen.py line 7. Also move or reuse is_dark_mode from
automate_title_screen through a Windows-free helper so the test does not import
pywinauto on non-Windows systems.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 53f6a1ab-e52a-4aa9-a0c0-7bb7abb86afb
⛔ Files ignored due to path filters (1)
tools/title-screen/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
.github/CODEOWNERS.github/workflows/generate-title-screen.yaml.gitignoreAGENTS.mdSPEC.mdtools/title-screen/.python-versiontools/title-screen/README.mdtools/title-screen/automate_title_screen.pytools/title-screen/capture_winutil.pytools/title-screen/create_composite.pytools/title-screen/inspect_winutil.pytools/title-screen/pyproject.tomltools/title-screen/test_automate_title_screen.pytools/title-screen/test_create_composite.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b4598c6c8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Thanks for the additional suggestions. I reviewed the collapsed nitpicks and am not planning to address the following items in this PR:
Please disregard these items for this PR. |
|
Tip For best results, initiate chat on the files or code changes.
I will disregard the collapsed nitpicks for this PR. The current scope and validation approach are acceptable. ʕ´•ᴥ•`ʔ ✏️ Learnings added
You are interacting with an AI system. |
Tested image is correct, SHA-256 hash is identical to the previously verified output.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
tools/title-screen/capture_winutil.py (2)
164-191: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winDeselect the bitmap before calling
GetDIBits.GetDIBitsrequires the bitmap to be unselected from every DC. Restoreprevious_bitmapbefore line 374; the current code restores it only afterGetDIBitsreturns.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/capture_winutil.py` around lines 164 - 191, Update the bitmap capture flow around GetDIBits to restore previous_bitmap to the device context before calling GetDIBits, ensuring the captured bitmap is unselected first. Keep the existing cleanup behavior after GetDIBits while avoiding any use of the bitmap after it has been restored.Source: Coding guidelines
343-390: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDeselect
bitmapbefore callingGetDIBits.
bitmapremains selected intomemory_dcwhenGetDIBitsruns. Win32 requires the bitmap to be deselected first. Restoreprevious_bitmapbeforeGetDIBits, and track the selection state so cleanup does not restore it twice. Otherwise,GetDIBitscan return fewer scan lines andcapture_windowraises instead of generating the title-screen captures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/title-screen/capture_winutil.py` around lines 343 - 390, Update the bitmap-selection flow around capture_window so previous_bitmap is restored before calling GetDIBits, ensuring bitmap is deselected when pixel data is read. Track whether the bitmap has already been restored, and make the finally cleanup restore it only when still selected, avoiding a second restoration while preserving cleanup on earlier failures.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@tools/title-screen/capture_winutil.py`:
- Around line 164-191: Update the bitmap capture flow around GetDIBits to
restore previous_bitmap to the device context before calling GetDIBits, ensuring
the captured bitmap is unselected first. Keep the existing cleanup behavior
after GetDIBits while avoiding any use of the bitmap after it has been restored.
- Around line 343-390: Update the bitmap-selection flow around capture_window so
previous_bitmap is restored before calling GetDIBits, ensuring bitmap is
deselected when pixel data is read. Track whether the bitmap has already been
restored, and make the finally cleanup restore it only when still selected,
avoiding a second restoration while preserving cleanup on earlier failures.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bf030d1d-4808-4ef1-94ff-ae895a1af0c2
📒 Files selected for processing (4)
.github/workflows/generate-title-screen.yamlAGENTS.mdtools/title-screen/capture_winutil.pytools/title-screen/create_composite.py
🚧 Files skipped from review as they are similar to previous changes (3)
- AGENTS.md
- .github/workflows/generate-title-screen.yaml
- tools/title-screen/create_composite.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61d3db4522
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Type of Change
Description
This adds a tool and manual GitHub Actions workflow for updating the title screen used in the README and documentation. The same approach can be extended later to keep images of other WinUtil tabs up to date on the Astro website. I can look into that once this is merged and properly tested.
The workflow compiles and opens WinUtil, switches to the Tweaks tab, captures both themes, and creates the final Light and Dark image. If the image changed, it opens or updates a pull request containing only that PNG.
If the workflow fails, it uploads the available image and diagnostic files for troubleshooting. WinUtil is always closed when the run finishes.
This was first built and tested in the title screen generation proof of concept, then moved here and adjusted to use WinUtil's source and existing pull request process. Here's the screenshot that test workflow generated
The generated image PR could be automatically merged in the future. For now, it is intentionally left for manual review so we can monitor the hosted runs and confirm that the screenshots are reliable.
Issue related to PR