From 5c7f8c399ba7a8251c84fbe9d2806158a412b531 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 24 Aug 2026 20:42:52 +0200 Subject: [PATCH 1/3] Let the screenshot walk run the GUI in another language English.txt is the one catalog with no high bytes, and hts_isStringAscii() short-circuits pure ASCII, so an English walk never reaches the conversion LANG_LOAD does. Every shot it takes is ASCII by construction. --language takes the lang/ basename the combo lists, which for several languages is not the translated name, so Francais rather than French. A missing name now lists what was on offer instead of failing on the select. The two pages the walk revisits to set the mirror up were keyed by their tab caption, which every translation changes. They are keyed by a control each one owns instead: IDC_maxrate and IDC_connexion each appear in exactly one dialog, and find() matches only the visible page. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .github/workflows/screenshots.yml | 11 +++++++++-- tools/screenshot-walk.py | 30 +++++++++++++++++++----------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 6b5a74f..e52822f 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -21,6 +21,10 @@ on: run-id: description: 'windows-build run to take the binaries from (default: the build of this commit)' required: false + language: + description: 'lang/ basename to run the GUI in, e.g. Francais. English exercises no conversion.' + required: false + default: English permissions: contents: read @@ -119,14 +123,17 @@ jobs: shell: pwsh env: MODE: ${{ inputs.mode }} + LANGUAGE: ${{ inputs.language }} run: | python -m pip install --quiet pywin32 pillow pywinauto $script = 'screenshot-walk.py' $site = @('--site-host', 'www.example.com') + $lang = @() + if ($env:LANGUAGE) { $lang = @('--language', $env:LANGUAGE) } # The probe only answers whether the machine renders at all, and takes no site. - if ($env:MODE -eq 'probe') { $script = 'screenshot-probe.py'; $site = @() } + if ($env:MODE -eq 'probe') { $script = 'screenshot-probe.py'; $site = @(); $lang = @() } Write-Host "running $script" - python "tools\$script" --exe app\WinHTTrack.exe --out shots @site + python "tools\$script" --exe app\WinHTTrack.exe --out shots @site @lang - name: Upload the screens if: always() diff --git a/tools/screenshot-walk.py b/tools/screenshot-walk.py index b2d83e9..87fd490 100644 --- a/tools/screenshot-walk.py +++ b/tools/screenshot-walk.py @@ -327,12 +327,16 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): sheet, tabs, captions = open_options(main, pid, ids) count = win32gui.SendMessage(tabs, TCM_GETITEMCOUNT, 0, 0) print(f" options sheet {sheet:#x}: {count} tabs") - at = {} + # Keyed by a control each page owns, because the captions below are translated. + wanted = {"IDC_maxrate": rate, "IDC_connexion": connections} + owns = {} for i in range(count): win32gui.SendMessage(sheet, PSM_SETCURSEL, i, 0) time.sleep(0.6) page = slug(captions.get_tab_text(i)) - at[page] = i + for control in wanted: + if find(sheet, control_id=ids[control]) is not None: + owns[control] = i shots.take(sheet, f"{4 + i:02d}_options_{page}") gated(sheet, ids, count) # Eight parallel transfers, so the animation shows a mirror working rather than one @@ -340,11 +344,10 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): # rate cap goes up with it: eight transfers sharing the engine's 100 KB/s default # would still crawl. Each on its own page, since a control on a page that is not on # top is not visible to find(). - for page, control, value in (("limits", "IDC_maxrate", rate), - ("flow_control", "IDC_connexion", connections)): - if page not in at: - raise RuntimeError(f"no {page} tab to set the mirror up on") - win32gui.SendMessage(sheet, PSM_SETCURSEL, at[page], 0) + for control, value in wanted.items(): + if control not in owns: + raise RuntimeError(f"no options page owns {control} to set the mirror up on") + win32gui.SendMessage(sheet, PSM_SETCURSEL, owns[control], 0) time.sleep(0.6) set_text(find(sheet, control_id=ids[control]), str(value)) close_options(sheet, pid, IDOK) @@ -352,15 +355,18 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): reopen_options(main, pid, ids) -def run(pid, ids, shots, url, base_path): +def run(pid, ids, shots, url, base_path, language): # Only a first run offers the language, so a VM replay skips this shot rather than # failing on it. try: lang = wait(lambda: window_titled(pid, "About WinHTTrack"), "the language dialog", 25) shots.take(lang, "00_language_preference") combo = ComboBoxWrapper(find(lang, control_id=ids["IDC_lang"])) - print(f" languages: {combo.selected_text()!r} of {len(combo.item_texts())}") - combo.select("English") + names = combo.item_texts() + print(f" languages: {combo.selected_text()!r} of {len(names)}, picking {language!r}") + if language not in names: + raise RuntimeError(f"no {language!r} in the language list: {sorted(names)}") + combo.select(language) click(find(lang, control_id=IDOK, class_name="Button")) except Timeout: print(" no language dialog: not a first run") @@ -429,6 +435,8 @@ def main(): ap.add_argument("--site-port", type=int, default=8099, help="port to serve it on (0 picks a free one)") ap.add_argument("--base-path", default=r"C:\shots-mirror") + ap.add_argument("--language", default="English", + help="lang/ basename as the combo lists it, so Francais rather than French") args = ap.parse_args() ids = resource_ids(args.resource_h) @@ -442,7 +450,7 @@ def main(): exe = os.path.abspath(args.exe) proc = subprocess.Popen([exe], cwd=os.path.dirname(exe)) try: - run(proc.pid, ids, shots, site_url, args.base_path) + run(proc.pid, ids, shots, site_url, args.base_path, args.language) except Exception as e: print(f"FAILED: {e}") diagnose(proc.pid, args.out) From 4db67925d5d493b83b27025abf18888b5a0a2dc9 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 24 Aug 2026 20:53:14 +0200 Subject: [PATCH 2/3] Say why the tab lookup is keyed by control, correctly The French walk shows the tab captions in English: they come from CAPTION in the .rc, not the catalog, so they do not translate and the old lookup would have worked. Keying on a control each page owns is still right, because a display string is not a key, but the comment claimed a breakage that does not exist. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- tools/screenshot-walk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/screenshot-walk.py b/tools/screenshot-walk.py index 87fd490..efc8939 100644 --- a/tools/screenshot-walk.py +++ b/tools/screenshot-walk.py @@ -327,7 +327,8 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): sheet, tabs, captions = open_options(main, pid, ids) count = win32gui.SendMessage(tabs, TCM_GETITEMCOUNT, 0, 0) print(f" options sheet {sheet:#x}: {count} tabs") - # Keyed by a control each page owns, because the captions below are translated. + # Keyed by a control each page owns, not by the caption: the tab labels come from the + # .rc and stay English, but a display string is the wrong key for a lookup either way. wanted = {"IDC_maxrate": rate, "IDC_connexion": connections} owns = {} for i in range(count): From 10a865b05eaff2d1b6f02ad1bbbfa621d6bb0108 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 24 Aug 2026 21:09:42 +0200 Subject: [PATCH 3/3] Match the class too: 1051 names two controls on two pages resource.h gives IDC_pausebytes the same 1051 as IDC_connexion, and find() matches on the number, so the discovery loop wrote the flow-control entry twice and the later page won. It happened to be the right one only because MainTab.cpp adds Limits before Flow Control; swapping those two lines would have put the connection count into the pause-bytes box with nothing raised. Both wanted controls are combo boxes and pausebytes is an edit box, so the class settles it, as it already does for the low button ids. A second page claiming the same control now raises instead of overwriting. screenshots.md gains --language, and loses the claim that the walk falls back to master's binaries: #132 made it stop instead. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .github/workflows/screenshots.yml | 2 +- tools/screenshot-walk.py | 11 ++++++++--- tools/screenshots.md | 13 ++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index e52822f..8aeadc3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -22,7 +22,7 @@ on: description: 'windows-build run to take the binaries from (default: the build of this commit)' required: false language: - description: 'lang/ basename to run the GUI in, e.g. Francais. English exercises no conversion.' + description: 'lang/ basename to run the GUI in, e.g. Francais.' required: false default: English diff --git a/tools/screenshot-walk.py b/tools/screenshot-walk.py index efc8939..46173f1 100644 --- a/tools/screenshot-walk.py +++ b/tools/screenshot-walk.py @@ -327,8 +327,8 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): sheet, tabs, captions = open_options(main, pid, ids) count = win32gui.SendMessage(tabs, TCM_GETITEMCOUNT, 0, 0) print(f" options sheet {sheet:#x}: {count} tabs") - # Keyed by a control each page owns, not by the caption: the tab labels come from the - # .rc and stay English, but a display string is the wrong key for a lookup either way. + # Keyed by a control each page owns: tab labels come from the .rc and stay + # English, but a display string would be the wrong key regardless. wanted = {"IDC_maxrate": rate, "IDC_connexion": connections} owns = {} for i in range(count): @@ -336,7 +336,12 @@ def options(main, pid, ids, shots, connections=8, rate=2_000_000): time.sleep(0.6) page = slug(captions.get_tab_text(i)) for control in wanted: - if find(sheet, control_id=ids[control]) is not None: + # Both are combo boxes, and resource.h gives IDC_pausebytes, an edit box on + # another page, the same 1051 as IDC_connexion. Match the class as well. + if find(sheet, control_id=ids[control], class_name="ComboBox") is not None: + if control in owns: + raise RuntimeError(f"{control} is on pages {owns[control]} and {i}, " + "so its id does not name one page") owns[control] = i shots.take(sheet, f"{4 + i:02d}_options_{page}") gated(sheet, ids, count) diff --git a/tools/screenshots.md b/tools/screenshots.md index 3f51fc4..e01e103 100644 --- a/tools/screenshots.md +++ b/tools/screenshots.md @@ -22,9 +22,10 @@ a real site. ## Replay Run the **screenshots** workflow (Actions → Run workflow) and download the -`screenshots` artifact. It takes the binaries from the last green `windows-build` on -the branch you dispatch it from, falling back to master, so a new option page is shot -from the build that added it. `run-id` overrides that; `mode: probe` runs +`screenshots` artifact. It takes the binaries from the green `windows-build` of the +commit you dispatch it from, so a new option page is shot from the build that added it. +A branch with no build of its own stops rather than shooting master's binaries, so pass +`run-id` for a change that builds nothing. `mode: probe` runs `screenshot-probe.py` instead, which only answers whether the machine renders and captures at all — reach for it when the runner image changes or when moving to a VM. @@ -41,6 +42,12 @@ and in the mirror folder name. For publication, pass `--site-host` as the workfl port stays visible, since `http.sys` holds 80 on the runner and 80 is excluded from binding there. +`--language` runs the GUI in another catalog, as the workflow input of the same name +does. It takes the lang/ basename the first-run combo lists, which for several +languages is not the translated name, so `Francais` rather than `French`. English is +the default and the only catalog with no high bytes, so it is the one language that +exercises none of the conversion `LANG_LOAD` does. + ## Adding an option page Nothing to edit: the tabs are enumerated from the sheet and named after their caption,