Skip to content

feat(gui)!: replace the terminal interface with an egui desktop window - #48

Merged
SecretLUL merged 20 commits into
mainfrom
feat/egui-desktop-gui
Sep 5, 2026
Merged

SecretLUL merged 20 commits into
mainfrom
feat/egui-desktop-gui

Conversation

@SecretLUL

Copy link
Copy Markdown
Owner

What this changes

WinMedic's interactive front end moves from a ratatui terminal interface to an
egui/eframe desktop window. src/ui/ is deleted, src/gui/ takes its place, and
the TUI is not kept behind a flag.

Almost nothing else had to move. App was already pure state with no ratatui in
it, the engine already reported over channels any front end can drain, and exactly
one line outside the render layer named a UI library β€” the
crossterm::event::KeyCode import in app::input. That is now a toolkit-neutral
Key enum which the front end translates egui's events into, so the whole layered
dispatch table and its tests survived unchanged. src/app/, src/engine/,
src/modules/, src/safety/ and the headless CLI are untouched apart from stale
comments.

Three decisions worth reviewing on their merits:

  • The binary stays in the console subsystem. #![windows_subsystem = "windows"]
    would buy a GUI that never flashes a console and pay for it by breaking every
    headless guarantee: a Windows-subsystem process does not hold the shell, so
    winmedic --json > out.json returns before the file is finished, the documented
    exit codes arrive after whatever ran next, and release.yml can no longer
    capture winmedic.exe --version into the variable it compares against the tag.
    utils::console hands the console back at GUI start instead, and only when
    GetConsoleProcessList says this process is alone in it. The cost is a console
    window visible until main is reached.
  • One binary, not two. A separate console executable was the obvious
    alternative and was rejected because swap_in_place in utils::self_update is
    built around updating exactly one file, with one checksum and one signature.
  • MSRV 1.88 β†’ 1.95. The old floor came from ratatui β†’ instability β†’ darling
    and left with ratatui; egui 0.36 sets the new one. Verified against a real 1.95
    toolchain, not assumed.

Three pieces of state stopped being reachable once the drawing changed, and are
handled rather than left dangling: the log scroll keys and their offsets are gone
because the consoles are scroll areas now; / no longer switches on a
character-capture mode, since a window has a real text field bound to the same
string; and the settings pane now draws which row is selected and which of its two
lists owns the arrow keys, both of which the keyboard moves and neither of which
was visible.

Dropping ratatui also clears the three RUSTSEC advisories the audit job's comment
documented as unfixable (paste, 2Γ— lru). cargo audit now reports nothing.

Related issue

None.

Does it change what WinMedic does to a system?

  • No β€” documentation, CI, tests or refactoring only

No diagnostic or repair logic changed. Every module, every fix path, the audit
log, the registry backups and the restore-point handling are byte-identical; what
changed is which pixels the same state is drawn as.

Checks

  • cargo fmt -- --check
  • cargo clippy --locked --all-targets -- -D warnings
  • cargo test --locked β€” 549 tests
  • New behaviour is covered by tests using MockCommandRunner, not real commands

The nine ratatui render tests become nine egui_kittest tests asserting against
the accessibility tree, which is the more honest target: a label missing from it is
missing for a screen reader, not merely for a test.

What was verified, and how

Windows 11 26200, Rust 1.98.1, MSVC toolchain.

  • cargo fmt -- --check, cargo clippy --locked --all-targets -- -D warnings
    clean; 549 tests pass.
  • cargo audit clean against the new lock file.
  • cargo +1.95 check --locked --all-targets passes, so the declared MSRV is real.
  • cargo build --locked --release (LTO, panic = "abort") links, and the release
    binary opens its window and closes cleanly.
  • Headless contract exercised against the release binary:
    winmedic.exe --version is capturable into a shell variable (this is the exact
    step release.yml performs), and --json --scan delivers 135 KB of valid JSON
    through a pipe with exit code 2.

Four defects were found by that verification rather than by the compiler, and are
fixed here: the settings pane's focus hint used the arrow glyphs the terminal
version had and all three rendered as empty boxes, because egui bundles a Latin
text face; the confirmation dialog's primary button drew dark text on a dark
surface; the selected setting row and the arrow-key focus were invisible; and /
was handling keystrokes twice. every_character_the_window_draws_has_a_glyph
guards the first of those, and was itself checked by reintroducing the defect and
confirming it goes red β€” the first version of that test passed while the bug was
still on screen, because it read AccessKit's label and static text lives in
value.

Honest gaps. No full scan has been driven through the window end to end; the
Health Scan and Repair Center tabs are covered by tests with realistic run state
(a failed module, a partial repair pass, console output), but nobody has watched a
real scan progress in the GUI. The release binary grew to 7.9 MB from the bundled
fonts and the OpenGL renderer.

πŸ€– Generated with Claude Code

SecretLUL and others added 20 commits August 20, 2026 13:16
Implements whea_logger diagnostic module to query Windows Hardware Error Architecture events (Events 19, 18, 17, 47, 1), extract APIC ID/Core and PCIe BDF fault locations, provide ASPM power management and mdsched fixes, and integrate into engine and UI.
…ext scan

Three separate scans re-reported findings that a repair had already
handled, or that no repair could ever clear:

- Scheduled Tasks ignored a task's state. Disabling is the only thing
  the repair does, and Windows neither resets LastTaskResult nor
  restores a deleted program, so every task WinMedic switched off came
  straight back on the following scan. Disabled tasks are now skipped in
  both checks, and the disable reads the state back afterwards so a
  TrustedInstaller-owned task that accepts the command without applying
  it fails honestly instead of counting as repaired.

- System Cleaner raised a finding on any non-zero byte. Every directory
  it sweeps is one the system refills by itself - a service's next log
  line, Explorer's 129-byte $Recycle.Bin\desktop.ini shell stub, the
  next favicon a browser caches - so the findings reappeared seconds
  after a successful repair. Cleanup targets now need 10 MB (50 MB for
  browser caches) before they are worth reporting, and the Recycle Bin's
  shell stub is neither counted nor deleted.

- The DNS check ran `nslookup <name> 8.8.8.8`, pinning a public resolver
  and bypassing the machine's own. Networks that block outbound port 53
  produced a permanent critical finding that ipconfig /flushdns cannot
  possibly fix. It now queries the configured resolver for two
  independent names, parses the answer record rather than substring
  matching a header that is present on failure too, and re-queries after
  the repair so an unfixed resolver is reported as a failed repair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README claimed the cache cleaner covered Brave and Opera; the
discovery only ever walked Chrome, Edge and Firefox, so both browsers'
caches were left untouched on every run.

Brave is Chromium on the same `User Data\<profile>\<cache>` layout as
Chrome and Edge, so all three now go through one helper instead of two
copies of the same loop. Opera does not use a `User Data` root: each
installed flavour (Opera Stable, Opera GX Stable, Opera One, ...) gets
its own directory under `Opera Software` and that directory *is* the
profile, so the flavours are enumerated rather than named and a flavour
Opera ships next needs no code change. Both the LocalAppData and the
Roaming root are checked, since the caches moved between releases and an
upgraded machine still carries the old one.

Only `Default` and `Profile N` directories are collected, so Chromium's
shared state next to them - `Local State`, the user's dictionary,
safe-browsing lists - is never swept.

The progress line, issue title and fix steps name the browsers actually
covered, so the finding in the TUI matches what the sweep does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parses kernel minidumps directly (PAGEDU64/PAGEDUMP headers) without
external tools like WinDbg or BlueScreenView: stop codes, bugcheck
parameters and faulting drivers (e.g. nvlddmkm.sys) are extracted from
C:\Windows\Minidump and correlated with BugCheck Event 1001 and
Kernel-Power Event 41 from the System log.

Findings: crash_driver_fault (driver attribution with rollback plan),
crash_video_tdr (0x116/0x117), crash_memory_bugcheck (0x1A/0x50/...),
crash_bugcheck_history, crash_unexpected_shutdown (Event 41 without
bugcheck) and crash_stale_dumps (cleanup). Fixes open Device Manager
for driver issues, schedule mdsched.exe for memory-class stop codes
and remove analysed dumps via PowerShell.

Registered as the 11th module; module-count assertions and integration
tests updated, 20 new unit tests added.
…1.98

Clippy 1.98 promotes chunks_exact_to_as_chunks to -D warnings, but the
as_chunks return order differs between toolchain versions, so plain
index pairs keep the UTF-16LE scan stable on every toolchain.
`src/ui/` is gone and `src/gui/` takes its place. Almost nothing else had to
move: `App` was already pure state with no ratatui in it, the engine already
reported over channels any front end can drain, and exactly one line outside the
render layer named a UI library β€” the `crossterm::event::KeyCode` import in
`app::input`. That import is now a toolkit-neutral `Key` enum which the front end
translates egui's events into, so the whole layered dispatch table and its tests
survived the move unchanged. `src/app/`, `src/engine/`, `src/modules/`,
`src/safety/` and the headless CLI are untouched apart from comments that had
gone stale.

The binary stays in the *console* subsystem, deliberately.
`#![windows_subsystem = "windows"]` would buy a GUI that never flashes a console
and pay for it by breaking every headless guarantee the CLI help makes: a
Windows-subsystem process does not hold the shell, so `winmedic --json > out.json`
returns before the file is finished, the documented exit codes arrive after
whatever ran next, and `release.yml` can no longer capture `winmedic.exe
--version` into the variable it compares against the tag. `utils::console`
instead hands the console back on the way up, and only when
`GetConsoleProcessList` says this process is alone in it β€” freeing a console
shared with `cmd.exe` would detach WinMedic from the terminal it was typed into.
The cost is a console window visible until `main` is reached.

`main` is no longer `#[tokio::main]`. `run_gui` needs a runtime it can hold open
for the life of the window and building one from inside another panics, so each
branch now owns its own; the GUI ends with `shutdown_background()` so closing the
window never blocks on a DISM call that has not come back yet. The MSRV moves
1.88 -> 1.95: the old floor came from `ratatui -> instability -> darling`, which
left with ratatui, and egui 0.36 sets the new one.

Three pieces of state stopped being reachable once the drawing changed, and are
handled rather than left dangling. The log scroll keys and their offsets are gone
because the consoles are scroll areas the wheel and scrollbars drive. `/` no
longer switches on a character-capture mode β€” a window has a real text field bound
to the same string, so the flag is now `focus_search`, a one-shot request the
front end honours and clears. The settings pane draws which row is selected and
which of its two lists owns the arrow keys, both of which the keyboard moves and
neither of which was visible.

The nine ratatui render tests become nine egui_kittest tests asserting against the
accessibility tree, which is the more honest target: a label missing from it is
missing for a screen reader, not merely for a test. One of them is new.
`every_character_the_window_draws_has_a_glyph` exists because the settings pane
first shipped its focus hint as the arrow glyphs the terminal version used, and
all three reached the screen as empty boxes β€” a terminal inherits the system's
font coverage, a window ships its own, and egui's is a Latin text face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the front end from the terminal to a window. The badge, the title, the
overview sentence and the MSRV all moved; so did the reason CONTRIBUTING gives
for the MSRV, which named the `ratatui -> instability -> darling` chain that no
longer exists.

Two shortcut rows are deleted rather than reworded. `[PgUp]`/`[PgDn]` and
`[Home]`/`[End]` scrolled the live log consoles, which are scroll areas now that
the wheel and their own scrollbars drive; there is nothing left for those keys to
move. `[Tab]` becomes `[Ctrl+Tab]` because bare Tab belongs to the window, where
it walks the focus between controls.

The headless section now says out loud what the console subsystem is there to
guarantee β€” that exit codes, pipes and redirects behave the way a script expects
β€” since that is the whole reason the GUI is not a Windows-subsystem binary.

The prerequisites gain the MSVC build tools: `build.rs` needs the Windows SDK's
`rc.exe` to embed the icon and version resources, and a machine with only rustup
installed gets a link error rather than an explanation.

Not covered here: `assets/banner.jpg` still shows the terminal interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment above `cargo audit` enumerated three informational warnings β€”
RUSTSEC-2024-0436 `paste`, RUSTSEC-2026-0002 and RUSTSEC-2026-0253 `lru` β€” as
things arriving through ratatui that could not be resolved here. All three left
with ratatui, and `cargo audit` now reports nothing at all against the lock file.

The policy the comment defends is unchanged and still worth stating, so only the
list is replaced: a gate that goes red for reasons nobody can act on is a gate
people learn to ignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The banner is the one place the old front end survived as pixels: the tagline
read "ADVANCED WINDOWS DIAGNOSTICS & REPAIR TUI" and the fourth feature box read
"Blazing Fast Rust TUI". The previous commit noted both as uncovered.

Only the leading letter differs between TUI and GUI, and the banner already
contains a capital G in DIAGNOSTICS β€” same line, same face, same weight, same
drop shadow β€” so the replacement is lifted from the image rather than typeset
into it, which is what keeps it from reading as a patch. The feature box sets its
text at 0.84 the size and sits on a card with a vertical gradient, so that copy is
scaled and composited as a difference against the flat background it came from,
which carries the ink and the shadow onto a differently coloured surface.

Re-encoded at quality 100: at 95 the file dropped from 520 KB to 195 KB, which
for a picture that is mostly soft glow around a shield is a lot of gradient to
throw away for two letters. At 100 it lands on 523 KB, so the rest of the artwork
is preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.4.0's diagnostics arrive unchanged. The WHEA hardware error logger, the
crash dump analyser, the wider cleaner sweep, the scheduled task resolution,
the repaired-findings fix and scan state persistence all merged without a
conflict, because none of them ever reached the front end β€” the same seam that
made the move to egui cheap makes this merge cheap. What did conflict was the
terminal itself, in five files, and the window wins each time: the ratatui views
v0.4.0 had edited are deleted, `ensure_terminal_size` and `run_tui` go with
them, and the log scroll offsets they carried are egui's own scroll areas now.

Three things had to be carried across rather than dropped.

Page Up, Page Down, Home and End move the triage selection again. They are new
variants on the toolkit-neutral `Key`, translated in `gui::keys` the way every
other key is, and they page by a fixed ten rather than by the height of the
visible list: a terminal's list had one height, a window's changes as the user
drags the frame, and a key that travels a different distance each time is worse
than one that travels a predictable one. The dispatch test v0.4.0 wrote for
these keys came with them, on `Key` instead of `KeyCode`.

The reboot-pending badge is drawn again. `has_pending_reboot` and the state
behind it merged cleanly, but the two places that showed it β€” the header and the
issue list β€” were terminal widgets, so nothing in the window said a repair was
waiting on a restart. The header carries the badge and a pending row shows
`reboot` where an open one shows its checkbox, which is also what
`toggle_select_all_issues` skips over.

`App::new` now restores the last scan from `%APPDATA%`, which makes every test
that builds an `App` depend on whatever the machine it runs on has scanned. CI
never sees this and a developer's machine always does. The GUI harness starts by
clearing that restored scan, next to the elevation prompt it already clears for
the same reason: these tests describe the findings they mean to draw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@SecretLUL
SecretLUL merged commit 7982bf8 into main Sep 5, 2026
4 checks passed
@SecretLUL
SecretLUL deleted the feat/egui-desktop-gui branch September 5, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant