Skip to content

+ a Defragment ribbon tab gives the block map the whole window - #439

Merged
Hawkynt merged 16 commits into
mainfrom
feat/defragment-ribbon-tab
Oct 7, 2026
Merged

Hawkynt merged 16 commits into
mainfrom
feat/defragment-ribbon-tab

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

What changed

Maintenance is no longer a separate dialog. A contextual Disk Tools → Defragment ribbon tab appears while the open archive/image, or an archive file selected in a folder, supports any maintenance operation. Selecting it hides the browser (hidden, not rebuilt, so leaving the tab restores it exactly) and gives the whole client area below the breadcrumb bar to the block map, with an optional files panel, the colour key and a progress line.

Group Items
Operation Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble (radio group); Start; Stop while extents are being moved
Defrag Mode Consolidate (+ Pack at End), Defrag, Re-order, Carve Hole (size in KiB, End/Offset, offset in KiB — shown only while picked) — extent moves — and Sort Entries (directories sorted by name in place, no data moved; FAT and exFAT)
Options Interleave (spinner 1–256), Metadata placement and Layout profile (combo boxes) + Edit Profiles for extent moves; Seed (Scramble) and Method (Optimize: Compress / Repack / Canonicalize, as the format offers) appear only with their operation
View Blocks / Circle / 3D Stack (→ BlockMapControl.ViewMode), Files panel, Legend, Analyze

Owner's names → engine: sort entries → SortDirectoryEntries, optimize → Compress/Repack/Canonicalize as offered, consolidate → ConsolidateAtStart (Pack at End → ConsolidateAtEnd), defrag → FillHolesLazy, re-order → AscendingOrder, carve hole → CarveHole, clear → wipe-empty, purge/optimize/shrink → the existing verbs.

Sort Entries is backed by the registry's SortDirectoryEntries operation (#440, IFilesystemDirectoryOrderer), offered for FAT and exFAT and run through MaintenanceOperations.SortDirectoryEntries.

Design

  • One adapter, now a view of the registry — Compression.NativeUI/Maintenance/TargetCapabilities.cs is the only place that turns the registry's answer into the ribbon's terms. That answer is MaintenanceCapabilities.Describe → MaintenanceProfile (Maintenance operations split by effect: compress, canonicalize, repack, sort entries, defragment extents, change geometry #440): operations plus the honoured DefragFeatures. The adapter was renamed from MaintenanceCapabilities so it no longer collides with the registry type. The probe code, the format-id lists and the ad-hoc Optimize routes are gone. Defragment = DefragmentExtents or SortDirectoryEntries. Optimize = Compress / Repack / Canonicalize, run through MaintenanceOperations (staged, verified, atomic). Shrink, Purge, Clear and Scramble run through the object carrying the capability. Minimal Geometry is gone, because Compact refuses it now. A test holds the adapter to the profile for every registered format.
  • DRY — the window's non-UI logic moved into MaintenancePresenter (choices → DefragOptions, validation, running each verb off the UI thread with live progress) and BlockMapSnapshot (layout reading). A NotSupportedException ends as Refused: the status line reports it, a message box explains it, and the image is untouched. After a change the archive is re-listed in place (ReloadArchiveInPlace). MaintenanceSession keeps the old nested-archive behaviour: the archive is maintained in a temporary copy and written back into the host.
  • Removed: DefragmentWindow (2,221 lines) and the Archive tab's verb buttons (that tab keeps Reconfigure). Tools → Maintenance (new shortcut Ctrl+Shift+D) and the list's Maintenance context entries open the tab with their operation picked. --screenshot=maintenance now shows the shell on the Defragment tab. Nothing else opened the window; PartitionsWindow never referenced it.
  • While you browse a folder, the archive you left is no longer a maintenance target. Before, a folder with nothing selected still offered maintenance on that closed archive.

NativeForms (pin bumped to 1e14385)

The first round worked around missing ribbon fields with hosted TextBox/ComboBox controls. NativeForms 1e14385, now pinned in its own commit, adds owner-drawn RibbonComboBox and RibbonSpinner: always one row tall, and still usable from a collapsed group's popup. Win32 Size now means the client area. The tab uses those fields throughout: Interleave is a 1–256 spinner; Metadata, Profile and Chunks are combo boxes; Carve Hole takes a size in KiB, End/Offset, and an offset in KiB; Seed is a spinner. No RibbonHostItem is left on the tab. With the frame outside the client area, the window bottom, the colour key and the status strip are fully visible at 150 %. No manual frame compensation was found in the shell or its dialogs. The headless test backend keeps its real combo-box peer, because IComboBoxPeer.SetItems takes a ReadOnlySpan that DispatchProxy cannot box. One gap remains: RibbonGroup has no Visible.

Review fixes (Linux run by the coordinator)

  • The GTK combo boxes were clipped at the bottom. They are now ribbon fields; a test asserts every input on the tab is a one-row RibbonFieldItem and none is hosted.
  • The Frag column showed "—" for every file. FatExtentMap reports the stored 8.3 name (BIG.BIN), while the lister applies the lowercase flags (big.bin). Names are now matched exactly, then case- and slash-insensitively when the match is unambiguous. Runs are counted in file order, before the extents are sorted for drawing. A test uses upper- and lowercase FAT fixtures and checks the counts against the extent map. FatExtentMap itself is unchanged: it still ignores the NT case bits, and the defrag mover keys on those names.
  • The run log was cut off at the right edge. It is now an owner-drawn LogView that wraps lines to its width and scrolls back with the mouse wheel; a test covers the wrapping.

Verification

  • MaintenancePresenterTests (44): FAT, ZIP (no Defragment, per # maintenance verbs and R/W are lossless or refused; real-volume evidence matrix #432) and partitioned-disk (nothing offered) capabilities. Interleave boundaries 0/1/256/257/non-numeric/empty, hole-size and offset parsing. Strategy → engine-mode mapping. A refused option never reaches the engine. Defragmenting a scrambled FAT keeps the image size and every file byte-identical and reports no fragmentation afterwards. An oversized carve hole and Defragment on a ZIP are both refused with the image byte-identical.
  • Shell tests (headless, MainFormTests.Defragment.cs): the tab is hidden with no target, shown for FAT, hidden again for a folder and for a partitioned disk. The client area swaps to the block view and comes back with the splitter and the folded navigation pane as they were. Items are enabled per format (FAT vs ZIP) with reasons in their tooltips. Defrag modes and layout options go grey for other operations. The carve fields appear only for Carve Hole. The interleave spinner clamps 0→1 and 257→256 and its stride reaches the engine; the carve size and offset reach the engine as bytes. The view toggles change ViewMode. Context-menu verbs open the tab with that operation picked. Start defragments in place. A refusal leaves the image byte-identical. The legend is shown on entry. The no-duplicate-shortcut test stays green, and Ctrl+Shift+D is pinned.
  • Sort Entries: on an unsorted FAT volume the root ends up in name order, and the size and every file are byte-identical. This is checked both through the presenter and from the ribbon, where the layout options grey out because a sort moves no data. fsck.fat -n -V accepts the sorted volume (ExternalFsInterop, WSL; passed locally). A test also holds the adapter to MaintenanceCapabilities.Describe for every registered format.
  • After rebasing onto main with Maintenance operations split by effect: compress, canonicalize, repack, sort entries, defragment extents, change geometry #440: 600/600 pass locally across Compression.Tests.NativeUI, Compression.Tests.Ui, Compression.Tests.Maintenance and the directory-orderer tests, against NativeForms 1e14385. CI passed on ubuntu-latest and windows-latest at 40b733f8.
  • Visual check: the Linux/WSL run could not be done from this environment (its sandbox refuses wsl/powershell invocations). The Win32 build was captured with PrintWindow (no input synthesized) at 1180×680 and 1700×900, over the FAT fixture, in all three view modes, with Carve Hole, Scramble and Compact picked. The ribbon fits at its natural height, the groups are readable, and the block view fills the client area in Blocks, Circle and 3D Stack. That check found the colour key switched off on entry, which is fixed and pinned by a test. The Linux (GTK, Xvfb) capture came from the branch-screenshot job, docs/screenshots/maintenance.png, and I inspected it. All four groups fit at the ribbon's natural height and the block map, files panel and colour key fill the client area. It also showed GTK combo boxes spilling under their captions, fixed by shorter metadata choices and a wider field. After the field change the latest Linux capture shows Interleave, Metadata "Unchanged" and Profile "(none)" whole, and the Windows capture at 150 % shows the full window.

@Hawkynt
Hawkynt force-pushed the feat/defragment-ribbon-tab branch 7 times, most recently from 8d2deaf to c43fbb5 Compare October 7, 2026 15:54
Hawkynt added 13 commits October 7, 2026 18:54
Maintenance lived in a separate 2200-line dialog that squeezed the block
map between an image picker, a strategy group, an output box and two rows
of buttons. It is now a contextual "Disk Tools" tab, Defragment, shown
while the open or selected image supports any maintenance operation.
Selecting it hides the browser (not torn down, so leaving restores it
exactly) and gives the client area to the block map, files panel, colour
key and a progress line.

Ribbon groups:
- Operation: Defragment, Optimize, Shrink, Compact, Clear, Purge,
  Scramble as a radio group; Start; Stop where cancellable.
- Defrag Mode: Consolidate (+ Pack at End), Defrag (FillHolesLazy),
  Re-order (AscendingOrder), Sort Entries, Carve Hole with size/offset
  fields shown only while picked.
- Options: interleave (1-256, refused outside rather than clamped),
  metadata zone, layout profile + Edit Profiles; seed, chunk placement
  and minimal geometry appear with their operation.
- View: Blocks / Circle / 3D Stack, Files, Legend, Analyze.

Every item asks one adapter, MaintenanceCapabilities, which is the only
place that maps registry interfaces to answers; modes and layout options
are probed against the descriptor's own refusal guard with a stream that
throws on first touch, and only for descriptors that implement the
options overload themselves (the interface default would accept and
ignore them). Sort Entries is answered "no" for every format: no
descriptor offers it through the registry yet. Optimize is offered only
where an optimizer changes something (ZIP/gzip/zlib/MacBinary/compound
tar re-encode, 7z solid regrouping, CVF clusters, file-internal chunks),
not where ArchiveOperations.Optimize would copy the file through.

The window's non-UI logic moved into MaintenancePresenter (choices ->
DefragOptions, running each verb off the UI thread, progress) and
BlockMapSnapshot (layout reading); a refusal (NotSupportedException) is
reported and leaves the image untouched, a change re-lists the archive
in place, and a nested archive is maintained as a temporary copy written
back after each change (MaintenanceSession). DefragmentWindow is gone;
Tools > Maintenance (Ctrl+Shift+D) and the list's Maintenance context
entries open the tab with their operation picked, and the Archive tab
keeps only Reconfigure. While browsing a folder, the archive left behind
is no longer a maintenance target.

The headless test backend gains a real combo-box peer: the interface
takes a ReadOnlySpan, which a dispatch proxy cannot box.

Verified: FAT, ZIP and partitioned-disk capabilities; interleave and hole
parsing boundaries; strategy -> engine mode mapping; tab visibility,
client-area swap and restore, per-capability enabling, carve fields,
view toggles; Start on a scrambled FAT keeps size and every file
byte-identical and reports no fragmentation afterwards; an oversized
carve hole is refused with the image byte-identical.
On GTK the field shows the end of a long entry, so "Don't move metadata" read as "metadata". The choices are one or two words now; the tooltip explains each.
GTK draws a combo box at its natural width, wider than the 88 px the field gave it, so it spilled under its caption and showed "nchanged". The input part is 116 px now.
Symptom: on GTK the Metadata and Profile fields showed only the top of "Unchanged" and "(none)".

Root cause: GTK draws a native combo box at its natural height whatever bounds it is given, and that is taller than a ribbon row.

Fix: the fields are an owner-drawn ChoiceBox (a DropDownButton whose menu holds the choices), which keeps exactly the bounds the ribbon lays out. A test pins every hosted input inside its row and rules out native combo boxes there.
Symptom: "SUCCEEDED (31 ms) — Defragmentation complete — 4…" ran off the right edge of the log under the files panel.

Root cause: a multiline TextBox neither wraps nor scrolls sideways on every backend.

Fix: the log is drawn by LogView, which wraps each line to its width (at spaces, inside a word only when the word alone is too wide), keeps the newest line in view and scrolls back with the mouse wheel.
Symptom: on a FAT16 volume written by Linux the Frag column read "—" for every file while the status line said one file was fragmented.

Root cause: the counts were keyed by the layout walker's names and looked up by the lister's. FatExtentMap reports a short entry's stored 8.3 name (BIG.BIN) and ignores the lowercase flags that the lister applies (big.bin), so no name matched. The runs were also counted after the extents had been sorted by offset, which merges a file stored backwards into one run.

Fix: an exact match is tried first, then a match ignoring case and a leading slash, used only when it is unambiguous. Runs are counted in the order the walker yields, which is the file's own order. Tests cover upper- and lowercase FAT fixtures against the extent map's own counts, the lookup rules, and log wrapping.
…bo boxes and spinners and keeps the window frame outside the client area
NativeForms 1e14385 draws RibbonComboBox and RibbonSpinner itself: always one stacked row, and still usable from a collapsed group's popup, which a hosted control cannot follow into. They replace the hosted text boxes and the owner-drawn ChoiceBox.

- Interleave is a spinner over 1-256, so an out-of-range stride can no longer be typed; it clamps.
- Metadata placement, layout profile and chunk placement are combo boxes.
- Carve Hole takes a size in KiB, a placement (End / Offset) and an offset in KiB, which is enabled only for Offset.
- Seed is a spinner.

Tests: every input on the tab is a one-row ribbon field and none is a hosted control; the interleave spinner clamps 0 and 257; the carve fields reach the engine as bytes.
…tries sorts FAT and exFAT directories

The tab's adapter guessed capabilities itself. It probed IArchiveDefragmentable with a stream that throws on first touch, and decided Optimize from a list of format ids. The registry now answers the question directly (MaintenanceCapabilities.Describe -> MaintenanceProfile). The adapter, renamed TargetCapabilities so it no longer shares the registry type's name, is now a view of that profile.

- Defragment: offered for DefragmentExtents or SortDirectoryEntries. The extent strategies and the interleave, metadata and profile options follow the profile's DefragFeatures. Sort Entries runs MaintenanceOperations.SortDirectoryEntries and moves no data, so the layout options do not apply to it.
- Optimize: Compress, Repack or Canonicalize, as offered, chosen in a Method combo. Each runs through MaintenanceOperations: staged, verified, replaced atomically. The ad-hoc routes (re-encode, 7z regrouping, CVF, chunk placement) are gone with the probe code.
- Shrink, Purge, Clear, Scramble and Compact: run through the object that carries the capability (IArchiveShrinkable, IArchivePurgeable, IWipeEmpty). Minimal Geometry is gone because Compact now refuses it; changing geometry is its own operation, offered nowhere yet.

Tests: the adapter agrees with the profile for every registered format. Sort Entries is offered for FAT and exFAT, not NTFS or ZIP. On an unsorted FAT volume it puts the root in name order with size and every file byte-identical, both through the presenter and from the ribbon. fsck.fat -n accepts the sorted volume (ExternalFsInterop, WSL).
…its name

Symptom: with f19.bin selected inside an open FAT16 volume, the tab worked on f19.bin as a BIN/CUE image. It showed "Real archive layout — 0 regions, 12.6 KB wasted" and an empty files panel, and disabled nearly every operation. The open volume should have been the target.

Root cause: an entry inside the open archive became the target by DetectByExtension on its name alone, ahead of the open volume.

Fix:
- An entry of the open archive is a candidate only when its content proves it a container. It is opened with ArchiveOperations.OpenEntry and identified with FormatDetector.DetectByContent on its head (up to 1 MiB when the entry stream cannot seek); the verdict is cached per archive, timestamp and entry. A file on disk is still judged by DetectCached.
- A Target field on the tab lists "Open volume (disk.img)" and, when proven, "Selected: inner.img (Fat)". The open volume is the default; the list's context-menu verbs aim at the proven selection.
- The status line names the target: "disk.img — FAT Filesystem Image · 16.0 MB".
- Changing the selection on the tab never retargets. It only adds or removes the offer, a chosen selected target stays listed, and nothing is re-evaluated while an operation runs. The tab moves to another volume only when the open volume it worked on is no longer open.

Tests: a plain .bin selected inside a FAT volume leaves the volume as the target with every FAT operation enabled. A real FAT image inside a ZIP is offered and becomes the target when picked. fake.img (plain bytes) and notes.txt are not offered. A selection change keeps the chosen target and the picked mode.
Symptom: at 1180 px on GTK the new Target group, one combo over two empty rows, pushed Options and View into drop-downs, hiding interleave and the view modes the owner wants directly visible.

Fix:
- The target field is a small row of the Options group, under Profiles…, shown only when there is a choice to make. The header line always names the target.
- Start leads the Operation group and Stop takes the free slot under Scramble, which saves a column.
- "Edit Profiles" is now "Profiles…".

A headless test asserts that no group collapses at the shell's default 1180 px width, both on a plain FAT volume and in the widest state (a nested target offered while Optimize shows its method). Widening a field makes it fail.
@Hawkynt
Hawkynt force-pushed the feat/defragment-ribbon-tab branch from a3fddf6 to 1a3bf39 Compare October 7, 2026 17:06
@Hawkynt
Hawkynt merged commit 385bc08 into main Oct 7, 2026
5 checks passed
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