Windows port, part 3b: the desktop app compresses on Windows, Windows settings, single instance and toasts (W3 part 2) #34

Manually merged
SkyfaR merged 36 commits from windows-w3b into main 2026-10-06 07:43:08 +02:00
Owner

Windows port, phase W3, part 2: the desktop app does its work on Windows. It builds on #32 (W3 part 1) and is independent of #33 (CI and AccessKit); both touch scripts/windows/system-dlls.txt and the docs, so whichever merges second gets a small conflict.

Compress, decompress and analyze from the app (src/wofrun/, src/gui/worker/wof_step.rs)

  • Shared run logic: the Windows run logic moved from the command line into the library (wofrun), so the app and ogc share it:

    • refusals and needs-admin;
    • the algorithm and the switch rule;
    • stored_with and the record rules, and files left for later;
    • DirectStorage and full drives.

    ogc's output is unchanged.

  • The app's worker runs it per game, with progress, cancel, pause for a running game and the history. It never elevates.

  • Card states (plan §6):

    • "Benötigt Administratorrechte";
    • "Bereits von Windows komprimiert (LZX)", also when another tool did it;
    • "Alte NTFS-Komprimierung (LZNT1)";
    • "Von der Xbox-App verwaltet", as cards of their own;
    • "Nicht komprimierbar" with the engine's reason, and a DirectStorage note.

    These facts survive a restart through an F line in analysis.tsv, which is written only on Windows; Linux's file is byte-identical.

  • Exact sizes after compressing (no "≈"), and the sidebar names the engine and the chosen algorithm ("NTFS · XPRESS8K").

Windows settings

  • Algorithm choice: "Komprimierungsverfahren" replaces the level slider. The four entries are "Schnell" (XPRESS4K), "Ausgewogen (empfohlen)" (XPRESS8K), "Stark" (XPRESS16K) and "Maximal" (LZX), each with a help text. It is stored as level=wof:xpress8k in gui.conf.
    • It is what every compression from the app uses (card, menu, COMPRESS ALL, history).
    • Estimates follow it: they are keyed by codec, so a change re-estimates.
    • It is what the sidebar shows.
    • The card menu offers the four algorithms instead of zstd levels.
  • Background task: "Hintergrund-Aufgabe (Aufgabenplanung)" is wired to the Task Scheduler's task: on/off, trouble and repair, last and next check, CHECK NOW via schtasks /Run, and the algorithm for new games.
  • Dark mode: light or dark follows Windows' AppsUseLightTheme where libadwaita does not.
  • Fonts: the bundled fonts are also registered for the process with AddFontResourceExW(FR_PRIVATE). Under Wine the Chakra Petch headings lose some glyphs either way; that is Wine's rasterizer. A real PC will tell.

App shell (src/gui/windows_shell/, src/gui/notification.rs)

  • One app per user:
    • The app is NON_UNIQUE, plus a mutex Local\OpenGameCompressor-<hash of SID>.
    • It serves a pipe whose DACL grants only the user and that refuses remote clients. A second start checks the pipe's owner before writing.
    • A second start forwards its command line (e.g. --show-page=history) within a 10 s deadline and exits; both ends have read timeouts.
  • Toasts:
    • Notifications become WinRT toasts with the AUMID LevelXStudios.OpenGameCompressor. On first start the portable app registers the AUMID and an opengamecompressor: URI scheme under HKCU, which the uninstaller removes.
    • A click opens the history.
    • CANCEL works only with a random per-job token, so a web page cannot cancel a job through the scheme.

Tests

  • Linux: fmt, both clippy runs, and cargo test --all-features with Broadway and OGC_TEST_REQUIRE_BTRFS=1 (1533 passed, 0 failed).
  • Windows (container):
    • check.sh and wine-test.sh;
    • check-gui.sh and build-gui.sh;
    • gui-test.sh: the app's Windows parts under Wine, including the real mutex, pipe and registry;
    • the installer tests;
    • gui-smoke.sh: a second start forwards to the running app, and the sidebar reads "NTFS · XPRESS8K".
  • Each piece had a review with adversarial verification, then a fix round; the final review found nothing more.

Not verified yet (needs a real Windows PC)

  • Compressing from the app on real NTFS (under Wine every folder is refused, as Wine has no WOF).
  • schtasks /Run and real task dates in other locales.
  • Toasts and their click.
  • The fonts, and dark mode through libadwaita itself.
Windows port, phase W3, part 2: the desktop app does its work on Windows. It builds on #32 (W3 part 1) and is independent of #33 (CI and AccessKit); both touch `scripts/windows/system-dlls.txt` and the docs, so whichever merges second gets a small conflict. ## Compress, decompress and analyze from the app (`src/wofrun/`, `src/gui/worker/wof_step.rs`) - **Shared run logic:** the Windows run logic moved from the command line into the library (`wofrun`), so the app and `ogc` share it: - refusals and needs-admin; - the algorithm and the switch rule; - `stored_with` and the record rules, and files left for later; - DirectStorage and full drives. `ogc`'s output is unchanged. - **The app's worker** runs it per game, with progress, cancel, pause for a running game and the history. It never elevates. - **Card states** (plan §6): - "Benötigt Administratorrechte"; - "Bereits von Windows komprimiert (LZX)", also when another tool did it; - "Alte NTFS-Komprimierung (LZNT1)"; - "Von der Xbox-App verwaltet", as cards of their own; - "Nicht komprimierbar" with the engine's reason, and a DirectStorage note. These facts survive a restart through an `F` line in `analysis.tsv`, which is written only on Windows; Linux's file is byte-identical. - **Exact sizes** after compressing (no "≈"), and the sidebar names the engine and the chosen algorithm ("NTFS · XPRESS8K"). ## Windows settings - **Algorithm choice:** "Komprimierungsverfahren" replaces the level slider. The four entries are "Schnell" (XPRESS4K), "Ausgewogen (empfohlen)" (XPRESS8K), "Stark" (XPRESS16K) and "Maximal" (LZX), each with a help text. It is stored as `level=wof:xpress8k` in `gui.conf`. - It is what every compression from the app uses (card, menu, COMPRESS ALL, history). - Estimates follow it: they are keyed by codec, so a change re-estimates. - It is what the sidebar shows. - The card menu offers the four algorithms instead of zstd levels. - **Background task:** "Hintergrund-Aufgabe (Aufgabenplanung)" is wired to the Task Scheduler's task: on/off, trouble and repair, last and next check, CHECK NOW via `schtasks /Run`, and the algorithm for new games. - **Dark mode:** light or dark follows Windows' `AppsUseLightTheme` where libadwaita does not. - **Fonts:** the bundled fonts are also registered for the process with `AddFontResourceExW(FR_PRIVATE)`. Under Wine the Chakra Petch headings lose some glyphs either way; that is Wine's rasterizer. A real PC will tell. ## App shell (`src/gui/windows_shell/`, `src/gui/notification.rs`) - **One app per user:** - The app is `NON_UNIQUE`, plus a mutex `Local\OpenGameCompressor-<hash of SID>`. - It serves a pipe whose DACL grants only the user and that refuses remote clients. A second start checks the pipe's owner before writing. - A second start forwards its command line (e.g. `--show-page=history`) within a 10 s deadline and exits; both ends have read timeouts. - **Toasts:** - Notifications become WinRT toasts with the AUMID `LevelXStudios.OpenGameCompressor`. On first start the portable app registers the AUMID and an `opengamecompressor:` URI scheme under HKCU, which the uninstaller removes. - A click opens the history. - CANCEL works only with a random per-job token, so a web page cannot cancel a job through the scheme. ## Tests - **Linux:** fmt, both clippy runs, and `cargo test --all-features` with Broadway and `OGC_TEST_REQUIRE_BTRFS=1` (1533 passed, 0 failed). - **Windows (container):** - `check.sh` and `wine-test.sh`; - `check-gui.sh` and `build-gui.sh`; - `gui-test.sh`: the app's Windows parts under Wine, including the real mutex, pipe and registry; - the installer tests; - `gui-smoke.sh`: a second start forwards to the running app, and the sidebar reads "NTFS · XPRESS8K". - Each piece had a review with adversarial verification, then a fix round; the final review found nothing more. ## Not verified yet (needs a real Windows PC) - Compressing from the app on real NTFS (under Wine every folder is refused, as Wine has no WOF). - `schtasks /Run` and real task dates in other locales. - Toasts and their click. - The fonts, and dark mode through libadwaita itself.
The checks, the algorithm a folder gets, what a game is recorded with, the
files left for later, DirectStorage and each folder's run from the checks to
the record are now the library's wofrun, so that the desktop app can share
them. ogc prints what a run tells through wofrun::folder::Tell, as before.
The app tells such games apart on their cards: the compressor leaves those
files as they are.
The worker runs the run ogc compress makes (wofrun) for each game, with
progress, cancel and the pause for a running game, and notes it in the
history as the app's; decompressing checks and records as ogc decompress
does. Cards show the algorithm a game was compressed with, exact sizes,
files left for later, and why the engine leaves a game alone: files that
need administrator rights (the app never elevates), a refused folder, the
Xbox app's games. They tell games stored with WOF or LZNT1 already and
DirectStorage. Request::Compress carries the WOF algorithm asked for.
NTFS · XPRESS8K there, BTRFS · ZSTD:3 on Linux as before; the settings'
choice of an algorithm shows through Sidebar::set_codec.
A game of the Xbox app has nothing to do with it; a cached estimate of a game
on a drive Windows' engine does not compress no longer offers btrfs.
The app's CHECK NOW starts the systemd timer's service once on Linux;
Windows had no counterpart. scheduler::run_now asks schtasks /Run for
the user's task, which starts ogc-background.exe with the task's own
arguments and settings, so no second run starts while one goes on and
the last result is the Task Scheduler's as for any run. start_now does
it for the real Task Scheduler. The fake schtasks of the tests runs a
task that is set up and refuses one that is not.
The app's settings name what games are compressed with as `level`, a
zstd level. Windows chooses one of the WOF algorithms instead
(system::FEATURES.algorithms), and its choice is stored under the same
key in the codec's text form, `level=wof:xpress8k`; Linux still writes
the bare level as every version did. Either system reads the other's
file: a WOF algorithm is kept as `algo`, a level of btrfs' range as
`level`, and the default algorithm is XPRESS8K (owner decision D1).

GuiConfig moves into theme/config.rs, with its tests, as theme.rs grows.
Windows compresses with one of the four WOF algorithms, not a zstd
level, so its settings show an AdwComboRow "Komprimierungsverfahren" in
place of the level's presets (plan 3.3, 6): "Schnell" (XPRESS4K),
"Ausgewogen (empfohlen)" (XPRESS8K, the default), "Stark" (XPRESS16K)
and "Maximal" (LZX). The open list says under each name what it brings
and costs, and a row under the combo says it for the one chosen, with
Microsoft's name of it. Linux keeps its level as it was.

A new algorithm is told to the window (Action::AlgoChanged), which
saves it and asks for the estimates again, and it is what the timer
writes for new games once the choice is at rest, as a level is:
SettingsPage::codec() is a Codec of either system. The page is built
for system::FEATURES, or in the tests for either system
(SettingsPage::for_system), so Windows' page is tested on Linux too.
Windows' settings left auto-recompress out, as its rows were systemd's.
They now show the Task Scheduler's task (system::FEATURES.timer and
task_scheduler on Windows), headed "Hintergrund-Aufgabe
(Aufgabenplanung)": its switch sets it up or removes it, inside are what
is wrong with it, the last check with CHECK NOW and REPAIR, and new
games, which the task compresses with the algorithm chosen. The switch
of notifications stays out, as the task passes none, and a run that
ended before it could note how points to background.log rather than to
journalctl. The dashboard's switch, the offer after the first
compression and the header bars' status follow, as on Linux.

CHECK NOW starts a run of the task (schtasks /Run) and waits up to 10 s
for it to note itself in run.json, so the settings show it running.
schtasks writes the next and last run in the user's own format, which
settings/clock.rs reads: the year first or last, a 12-hour clock with
its AM or PM in any of the app's languages, and where a date does not
tell, the day and month in the order of the user's short date format
(GetLocaleInfoEx); systemd's form reads as before.

gui-test.sh runs the settings' tests of Windows under Wine too.
The desktop's theme was libadwaita's default color scheme, which
follows the desktop only where libadwaita can read its style. Under
Wine it cannot (system_supports_color_schemes is false), and the app
stayed light with Windows set to dark. Where it cannot, the theme now
reads Windows' "app mode" (AppsUseLightTheme below
HKCU\...\Themes\Personalize, plan 1.4) through platform::registry and
forces dark or light from it, and reads it again whenever the window
becomes active. Linux' registry is empty, so Linux is as before; a theme
chosen in the settings goes first as ever, and windows of tests that set
their color scheme themselves are left alone. Under Wine with the value
at 0 the app now starts dark.

ThemeChoice moves into theme/scheme.rs; the tests read a registry of
their own.
Pango's Win32 font map takes the bundled fonts through DirectWrite, and
Pango's cairo draws a font by its DirectWrite face where it gets one,
else through GDI by the font's name, where GDI would pick another font
for one it does not know (plan 1.4). On Windows the app now also adds
each bundled font for its own process (AddFontResourceExW, FR_PRIVATE)
at the start of main, before GTK reads which fonts there are. Nothing
is installed, and nothing changes elsewhere.

Under Wine this changes nothing visible: the headings in Chakra Petch
are drawn alike with and without it, the right letters in the right
places with D, H, M, N and O missing, so the font is found and Wine's
rasterizer drops those glyphs. How a real Windows draws them is still
to be seen there. A test under Wine has Windows take every bundled
font.
ARCHITECTURE.md: the algorithm row and the Task Scheduler's task in
Windows' settings, how gui.conf keeps the algorithm, the times schtasks
writes, CHECK NOW's schtasks /Run, the bundled fonts registered with
Windows, the light or dark style from Windows' setting, and what Wine
showed of both; the settings' parts are no longer listed as missing on
Windows.
The note on what decides the rows of each system had landed between two
groups of the list in the module's documentation; it follows the list.
A named mutex decides which start is the app; later starts hand their
command line (--show-page, or a toast's URI) over a pipe that only the
user may open, and end. GNotification has no Windows backend, so the
notifications become WinRT toasts of the AppUserModelID
LevelXStudios.OpenGameCompressor, whose clicks start the app through the
opengamecompressor: URI scheme the app registers for itself.
# Conflicts:
#	docs/ARCHITECTURE.md
# Conflicts:
#	docs/ARCHITECTURE.md
#	scripts/windows/gui-test.sh
#	src/platform/windows/scheduler.rs
schtasks writes times in the user's format, and Latin American Spanish
names the half of the day in two words, "a. m." and "p. m.", with a
space or a no-break space. Each word alone named no half, so 2 p.m. was
read as 2 a.m. A word joined with the next one now counts too.
AddFontResourceExW sets no last error, so the warning printed whatever
an earlier call had left. It now tells why the file cannot be read, or
nothing more.
A second start counted a hundred tries of 100 ms, but each try on a
busy pipe also waited a second for it: nearly two minutes without a
window instead of ten seconds. The wait is a deadline now, which the
wait for a busy pipe does not stretch.

Neither end of the pipe had a timeout. A start now waits for the app's
answer two seconds at most, and the app lets a start go that says
nothing for two seconds, or does not close its end once answered,
instead of holding its only pipe instance for it.

Pipe names are the whole computer's, and another account could make
the app's pipe first and receive every later start's command. A start
now checks that the user owns the pipe before it writes or lets the
other process take the foreground, and else becomes an app of its own
at once. The app makes its pipe with the user as owner, also when
elevated, and says when the pipe's name is taken.
The scheme opengamecompressor: is registered for the whole user, so a
link on any web page, mail or document could start
ogc-gui.exe --uri=opengamecompressor:cancel-job and cancel the job that
goes on in the background.

A toast with CANCEL now carries a random token of 128 bits in the
button's URI (cancel-job/<token>), which the app keeps while that toast
is out: made with its first send, kept by its updates, dropped once the
toast is taken back or sent without CANCEL. The app cancels only for
that token; any other CANCEL just brings the window to the front, as a
plain start does. Without random numbers the toast has no CANCEL. The
log does not show the token.
Every compression the window asks for passes the algorithm chosen in the
settings as Request::Compress.algorithm, and the sidebar names it at the
start and when it changes. Estimates are keyed by the codec they are for,
a zstd level on Linux as before and the WOF algorithm on Windows, in the
rows, the worker's requests and analysis.tsv, so that choosing another
algorithm estimates the games anew, as a new level does on Linux. Linux
writes analysis.tsv as before.
What a Windows analysis finds out besides the estimate (files stored with
another algorithm, LZNT1, DirectStorage) lived only in memory. A restart
took the estimate from analysis.tsv and asked for no analysis, so the
cards no longer told them. They are kept in an F line after the report,
written only where there is something to tell, so Linux' analysis.tsv
stays byte for byte as it was.
The card's "Compress at level…" offered btrfs' zstd presets on Windows,
whose engine takes none. There it reads "Compress with method…", and its
question offers the four WOF algorithms, which the window compresses the
game with. Linux keeps its levels.
The question before decompressing ended with what a drive mounted with
compress= does to files written later, which only btrfs has. Windows'
question ends before that sentence, in all 13 languages.
When excluded.tsv could not be read, the app's Windows jobs left every
game alone, which stays so, but each card said that the user had excluded
it. The card now names the error, as Linux' worker does, so that the user
has a hint at the file to mend.
src/wofrun/folder/tests.rs had grown to 530 lines when the run moved
into the library. The tests of compress_game, ogc auto's run of a game,
are now in auto_tests.rs beside it, with the same stand-ins, so that both
stay under 500 lines.
The window's file had grown over 500 lines. The handler of a card's main
button moves to window/actions.rs, where the window's other answers to the
cards are.
The request the window sends for a compression is built by a function of
its own now, so that a test can tell that a WOF algorithm becomes the
job's algorithm and a zstd level its level, as before.
Merge branch 'worktree-wf_72d5144a-5a5-2' into windows-w3b
All checks were successful
CI / Bundle for any distribution (pull_request) Successful in 40s
CI / Release (pull_request) Has been skipped
VM test / Migrations in a VM (pull_request) Successful in 17m56s
CI / Windows, command line (clippy with tests) (pull_request) Successful in 41s
CI / Windows, command line (tests under Wine) (pull_request) Successful in 1m37s
CI / Format, lint and test (pull_request) Successful in 5m37s
CI / Bundle programs, command line and helper (pull_request) Successful in 2m12s
CI / Bundle programs, desktop app (pull_request) Successful in 2m57s
CI / Arch package (pull_request) Successful in 3m5s
003798932b
SkyfaR manually merged commit 578c52afa4 into main 2026-10-06 07:43:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
LevelXStudios/OpenGameCompressor!34
No description provided.