# Working on ClutterCutter > Onboarding + work instructions for any human or AI agent picking this repo up > on a fresh machine. Read this first; it saves re-deriving the basics every > time. Keep it current — when a workflow, convention, or the app's status > changes, update this file in the same PR. ## What this is **ClutterCutter** — a lightweight Windows disk-usage browser by **Struis ICT**. Single-file native `.exe`, no installer. Lets you scan a drive and see what's eating space (tree + list, top largest files, oldest files, safe-to-delete temp caches), with MFT fast-path scanning when run as admin on NTFS. - **Repo:** https://github.com/StruisICT/ClutterCutter (org moved from `Struis112`; the org URL is canonical now) - **License:** MIT (`LICENSE` at root) - **Funding:** Buy Me a Coffee — `struis112` ## The build ClutterCutter is a single Rust crate with three frontends. CI builds it on every push and attaches the Windows GUI to every GitHub Release. | Binary | Source | Platform | Notes | |--------|--------|----------|-------| | **`cluttercutter`** (native GUI) | `rust/src/main.rs` + `gui/` | Windows only | `cluttercutter.exe` → shipped as `ClutterCutter.exe`; the shipping build (current UI + all features; **what winget packages**) | | **`cluttercutter-gui`** (egui GUI) | `rust/src/bin/gui/` | Linux / macOS / Windows | Portable eframe/egui frontend sharing the scan core; the cross-platform build | | **`cluttercutter-cli`** | `rust/src/bin/cli.rs` | Linux / macOS / Windows | Console harness for the scanners | One crate, three frontends. The scan **core** (`walk`, `analysis`, `types`, `datetime`, `drives`, `tempscan`, `format`) is portable and shared; the Windows-only pieces (native Win32 GUI, `FindFirstFileExW` scanner, NTFS MFT, the Explorer-style `temp` discovery) are gated behind `#[cfg(windows)]`, and the `windows` dependency itself is `cfg(windows)`-scoped. On Linux/macOS the portable frontends use the `walk` (std::fs) walker; the Win32 `FindFirstFileEx`/MFT fast paths stay Windows-only. `cluttercutter-gui` uses eframe's **glow** backend (no wgpu/DirectX). ## Repo map ``` ClutterCutter.ico/.png/.psd # icon + source art (build-icon.ps1 regenerates the .ico) build-icon.ps1 # regenerate ClutterCutter.ico from source art README.md # user-facing docs CHANGELOG.md # generated by release-please (don't hand-edit) rust/ # the app (Rust crate) Cargo.toml # crate = "cluttercutter"; `windows` dep is cfg(windows)-scoped; version synced by release-please (publish=false) src/lib.rs # module list (windows-only modules gated behind #[cfg(windows)]) # -- portable scan core (Linux/macOS/Windows) -- src/types.rs # FolderNode / FileEntry / ScanProgress src/walk.rs # portable std::fs recursive walker (mirrors scanner.rs semantics) src/analysis.rs # pure tree-walk queries: top_n_files, oldest_n_files (bounded heaps) src/datetime.rs # Win32-free FILETIME <-> date helpers src/drives.rs # volume enumeration (GetDiskFreeSpaceExW on Windows, statvfs on Unix) src/tempscan.rs # portable temp/cache location discovery src/format.rs # byte/count formatting + unit mode # -- Windows-only (#[cfg(windows)]) -- src/main.rs # native GUI entry -> gui::run() (Windows; prints a hint + exits elsewhere) src/gui.rs + src/gui/ # Win32 window/message-loop/WndProc, all views (~2.3k lines) src/scanner.rs # FindFirstFileExW recursive walker (LARGE_FETCH, parallel top-level fan-out) src/mft.rs # NTFS MFT parser via \\.\C: raw volume reads (admin fast path); FRN == Vec index (see below) src/temp.rs # Explorer-style safe-to-delete temp/cache discovery build.rs + app.rc # embeds icon/manifest via embed-resource (Windows targets only) # -- cross-platform frontends -- src/bin/cli.rs # console harness to exercise the scanners without a GUI src/bin/gui/main.rs # eframe/egui GUI (cluttercutter-gui): drives, browse/largest/oldest, search, temp src/bin/gui/palette.rs # egui theme (warm-white light + dark, ported from the Win32 palette) winget/ # winget-pkgs manifests + submission guide (winget/README.md) .github/workflows/ # build.yml (Windows), linux.yml (Linux), release-please.yml, commit-lint.yml, lint-workflows.yml ``` ## Build & run You only need a Windows machine and the Rust stable toolchain. CI mirrors these exact steps in `.github/workflows/build.yml`. ### Rust build (needs the Rust stable toolchain) ```powershell cd rust cargo build --release # -> rust/target/release/cluttercutter.exe (the GUI) cargo run --release # build + launch the GUI cargo clippy --all-targets # lint cargo fmt # format (keep tree fmt-clean) ``` ### egui GUI (cross-platform — Linux / macOS / Windows) ```sh cd rust cargo run --bin cluttercutter-gui # build + launch the portable GUI rustup target add x86_64-unknown-linux-gnu # once, to verify the Linux build from any host cargo check --target x86_64-unknown-linux-gnu # confirm the whole crate compiles for Linux ``` `cluttercutter-gui` shares the scan core with the Win32 app; it uses eframe's glow backend. glow renders straight to the GL surface, so OS window capture (PrintWindow) can't grab it — use these env helpers for headless verification: - `CC_SCAN=` auto-scan a folder on launch - `CC_SEARCH=` apply an initial search once the auto-scan lands - `CC_SHOT=` save one framebuffer screenshot to ``, then exit `.github/workflows/linux.yml` mirrors this: it builds both portable binaries on ubuntu, runs the portable tests, and uploads a `ClutterCutter-linux-x86_64` tarball (attached to the Release on a tag build). ### CLI harness (fast way to test scanners without the GUI) ```powershell cd rust cargo run --bin cluttercutter-cli -- C:\ # FindFirstFileEx walk of C:\ cargo run --bin cluttercutter-cli -- --mft C:\ # MFT fast path (run elevated) cargo run --bin cluttercutter-cli -- --top-n 20 C:\Users # 20 largest files cargo run --bin cluttercutter-cli -- --oldest-n 20 C:\Users cargo run --bin cluttercutter-cli -- --temp # enumerate safe-to-delete temp/caches ``` > MFT mode and the GUI's MFT fast path require **Administrator** (raw `\\.\C:` > volume access). The GUI prompts to relaunch elevated at startup if it isn't. > **MFT parse is parallel.** Each read-chunk's FILE records are parsed across all > cores via `thread::scope` (`parse_chunk_parallel`), because a record's FRN is > exactly its ordinal position — so entries live in a `Vec>` > indexed by FRN (no hashing) and the chunk's byte-slice + entry-slice split at > identical record boundaries with no shared state. Parsing is pure > (`parse_record`); only the read is serial. ~1.6× faster on a large volume > (C:\ ≈ 8.5s → 5.3s here), identical results (verified old-vs-new on a > quiescent volume). If you touch this, keep FRN==index and re-verify totals. ## Feature map (what the GUI implements) The GUI (`gui.rs`) implements: drive scanning with auto MFT-vs-walker selection, the DRIVES sidebar + table + clickable breadcrumb (see the Struis ICT redesign below; the folder tree still exists but is hidden and drives navigation), Dark/Light/Auto theme (incl. immersive dark title bar via DWM), the right-click context menu (Open in Explorer / Copy path / Cmd here / Recycle), and the keyboard shortcuts (F5 rescan, Esc stop, Backspace parent, Enter drill, Del recycle — Enter/Del act on the focused pane), plus the elevation prompt and About box. When adding a feature, keep the existing UX and interaction model consistent; if you intentionally change behavior, note it here. **Notable design points:** - **Struis ICT redesign (the app's chrome)**: three owner-drawn custom windows in `gui.rs` — `topbar_proc` (logo + "ClutterCutter" / "Struis ICT" + a theme-toggle pill), `sidebar_proc` (the left **DRIVES** column of usage-bar cards; the reparented Scan-all button lives at its bottom and its `WM_COMMAND` is forwarded to the main window; card clicks post `ID_DRIVE_BASE+i`), and `crumb_proc` (the breadcrumb path bar). The **folder tree is kept but hidden** (`SW_HIDE`) — it still holds all navigation state: double-click / Enter / breadcrumb-segment clicks call `TVM_SELECTITEM`, which fires `on_tree_select`. The breadcrumb rebuilds its segments each paint by walking the hidden tree's parent chain (`crumb_segs` stores per-segment hit rects). Brand palette lives centrally in `palette(is_dark) -> Pal` and tuned to match the mockup `index-selection.png`: **light chrome** (top bar / sidebar / status / panel header are `#EEF0F4` / `#FFF` / `#F7F8FA`, *not* brand-blue) with a two-accent split — **blue `#2D6BF0`** for drive usage bars, sizes and the active card border, **green `#70BB51`** for the table "% of parent" bars. Rounded shapes via `fill_round` / `card_round` (RoundRect). The logo is the Struis "S" mark (`struis-s.ico`, **icon resource 2** in `app.rc`, loaded with `load_logo_icon`); "Struis ICT" is a bordered chip; the theme pill is a rounded track with a dark knob + Segoe MDL2 sun/moon glyphs. Layout geometry: `TOPBAR_H`, `SIDEBAR_W`, `CRUMB_H`, `DRIVE_CARD_H`. The main table has custom-drawn columns (`custom_draw_main_list` paints the Name column's folder/file glyph + text and the green "% of parent" bar; `custom_draw_side_list` paints the side-panel Size column as blue text) — both via `NM_CUSTOMDRAW` routed through `on_notify`, with per-row selection read via `LVM_GETITEMSTATE` (`nmcd.uItemState` is unreliable at the sub-item stage). Gridlines are off. Fonts created in `create_children`: `font_title`, `font_small` (Segoe UI, for the brand's Raleway) and `font_icon` (Segoe MDL2 Assets glyphs). - **Side panel layout**: the View menu picks an optional extra view (**Top largest files**, **Oldest files**, **Safe-to-delete temp files**) shown in a right-hand panel. The panel header has a **Detach** button (also View ▸ Detach side panel) that floats it in its own resizable window — closing the float re-attaches. Theme sits in its own top-level menu (plus the top-bar pill). - **Scan all drives**: button next to the per-drive buttons; scans every volume (MFT where possible) into a synthetic "All drives" root. Shell actions no-op on the synthetic root (it has an empty path). F5 re-runs whichever scan (single or all) ran last. Also **runs automatically on startup** (kicked from `run()` right after the window shows), so the app opens straight into a populated tree. - **Parallel + incremental**: each drive scans on its own worker thread (volumes are independent → safe, and wall-clock ≈ the slowest drive, not the sum). Drives are appended to the root one at a time as they finish (`WM_APP_DRIVE_DONE` → `on_drive_done` → `append_drive`), so results appear progressively. The root's `children` Vec is pre-reserved to the drive count so these pushes never reallocate (which would dangle the raw child pointers the tree items hold) — **keep that reservation** if you touch this. - Drives are shown **alphabetically** (tree via `TVI_SORT`; the root's list via a name-sort special-case in `populate_list_folders` keyed on the empty synthetic path) and the root is **auto-expanded** (`set_tree_item_has_children` + `TVM_EXPAND`). - `WM_APP_PROGRESS` is **coalesced** (`progress_pending` AtomicBool, at most one in flight) so parallel scanners can't flood the queue and stall the UI. - **Recycle all** button in the temp-files panel: recycles every listed temp file in one undoable background shell operation and clears the list. - The **top/oldest side lists** support the full context menu + multi-select Del recycle (rows carry (folder, file) pointers via `side_hits`). - **In-place delete (no rescan)**: recycling never triggers a full rescan. The `SHFileOperationW` runs on a background thread (`recycle_in_background`), and the in-memory tree is updated in place: the deleted folder's pointer (and all its descendants') go into `deleted_nodes` — a tombstone set that every view skips (`populate_list_folders`, the top/oldest hit filter) — and ancestor `size`/`file_count`/`folder_count` totals are decremented via `subtract_along_ancestors`. **Nodes are never removed from a `children` Vec** (that would shift memory and dangle the raw pointers the tree items / `side_hits` hold) — they stay allocated and hidden until the next full scan (which clears `deleted_nodes`). If a background recycle reports failure, `on_recycle_done` falls back to a full rescan to resync. The ancestor-total and subtree-collection math is unit-tested (`subtract_along_ancestors`, `collect_folder_ptrs`). - **Accessibility (WCAG 2.2 AA)**: the message loop runs `IsDialogMessageW` (main + floating frame) so Tab cycles all controls. Dark mode themes the buttons (`DarkMode_Explorer`) and paints window/panel backgrounds via `WM_ERASEBKGND` (class brushes are fixed at registration). Keep these invariants when touching theming. - **Dark-mode plumbing** (`rust/app.manifest` + `gui.rs`): the embedded manifest declares comctl32 v6 + the Win10 supportedOS GUID — without it the process gets classic comctl32 v5.82 and `SetWindowTheme`/`DarkMode_*` are inert (this was why buttons/headers stayed white). Full dark rendering then needs the undocumented uxtheme ordinals (104/133/135/136 — guarded, no-op if missing), `ItemsView` on the listview header children, an owner-drawn menu bar via the `WM_UAHDRAWMENU`/`WM_UAHDRAWMENUITEM` messages (light mode falls through to the stock bar), and a custom status strip (`ClutterCutterStatus` class) replacing `msctls_statusbar32`, which has no dark theme part. Theme switches end with `SWP_FRAMECHANGED` + `RedrawWindow` so the non-client area repaints. ## Versioning (SemVer) Versions follow [Semantic Versioning](https://semver.org): `MAJOR.MINOR.PATCH`. The single project version (in `.release-please-manifest.json`) is the source of truth; the Rust crate version (`rust/Cargo.toml`) is kept in sync automatically. The project is **pre-1.0**, so per SemVer's 0.x clause the API isn't considered stable yet and breaking changes bump the **minor**. Conventional commits map to bumps like this (configured in `release-please-config.json`): | Commit type | Pre-1.0 (now) | Post-1.0 | |-------------|---------------|----------| | `fix:` | patch (`0.3.0` → `0.3.1`) | patch | | `feat:` | minor (`0.3.0` → `0.4.0`) | minor | | `feat!:` / `BREAKING CHANGE:` | minor (`0.3.0` → `0.4.0`) | major | | `docs/chore/build/ci/refactor/...` | no release on their own | same | Relevant release-please flags: `bump-minor-pre-major: true` (breaking → minor while 0.x) and `bump-patch-for-minor-pre-major: false` (so `feat:` → minor, not patch). `extra-files` bumps `rust/Cargo.toml` in lockstep. **`Cargo.lock`:** only `rust/Cargo.toml` is auto-bumped (the lock's package index is order-dependent, so bumping it by jsonpath would be fragile). The lock's own `cluttercutter` version line is regenerated by `cargo` on the next build, and CI doesn't pass `--locked`, so a one-version lag never breaks the build. If you want it tidy, run `cargo build` and commit the regenerated `Cargo.lock`. **Graduating to 1.0.0** is a deliberate product decision — do it when the behaviour/CLI is considered stable. Either set `bump-minor-pre-major: false` (the next breaking change then bumps to `1.0.0`) or land a commit with a `Release-As: 1.0.0` footer. After 1.0, breaking changes bump the **major** per strict SemVer. ## CI / release process - **`build.yml`** runs on every push/PR: builds the exe, smoke-tests it (launches, confirms it doesn't exit immediately, kills it), uploads an artifact, and — on a tag/Release — attaches the exe to the Release. - **Releases use [release-please](https://github.com/googleapis/release-please)** (`release-type: simple`, tags like `vX.Y.Z`, no component in tag). It keeps an open "release PR" updated from conventional commits on `main`; merging it tags + creates the Release. Version state lives in `.release-please-manifest.json` (currently `0.3.0`); `CHANGELOG.md` is generated — **do not hand-edit it**. - **Gotcha:** tags created by `GITHUB_TOKEN` don't trigger workflows (not even the `release:` event). release-please therefore runs with the **`RELEASE_PLEASE_TOKEN`** repo secret — a fine-grained PAT (this repo only; Contents + Pull requests read/write). Tags/Releases made with it trigger `build.yml` (attaches the exes) and `winget-manifest.yml` automatically. If the secret is missing or expired the workflow falls back to the workflow token and releases come out bare — then run **Actions → Build → Run workflow** with the tag, and **Actions → winget manifest** with the version. Rotate the PAT before it expires (github.com → Settings → Developer settings → Fine-grained tokens), then `gh secret set RELEASE_PLEASE_TOKEN`. - **Open release PR:** there's usually an auto "chore(main): release X.Y.Z" PR open — merge it when you want to cut that version. - **winget:** right after a release, the **winget manifest** workflow (`.github/workflows/winget-manifest.yml`) runs `scripts/Update-WingetManifest.ps1` to generate the `1.12.0` manifest set (SHA256 from the published asset) and open an **in-repo PR**. It never submits to `microsoft/winget-pkgs` — that copy-into-a-fork step stays manual. If the release was published by release-please (`GITHUB_TOKEN`) the workflow may not auto-start; run it from **Actions → winget manifest** with the version. Full steps + repo rules in `winget/README.md`. Packaged asset is `ClutterCutter.msi` (per-machine WiX installer, `ElevationRequirement: elevationRequired` — see winget/README.md Notes for why that flag matters); bump winget only for releases with Windows-facing changes. ## Code signing Release binaries are wired to be Authenticode-signed for **free** via [SignPath Foundation](https://signpath.org)'s open-source program (a verifiable publisher, forever). The signing step in `build.yml` (`signpath/github-action-submit-signing-request`) is **inert until configured**, so CI stays green unsigned. It runs only for releases/tags, signs the uploaded artifact, and the signed exes replace the unsigned ones before they're attached to the Release — so the winget SHA256 (computed from the published asset) matches the signed binary. One-time setup: 1. Apply to the **SignPath Foundation** OSS program (signpath.org) and get an organization. Create a **project** for ClutterCutter, an **artifact configuration** that signs `ClutterCutter.exe`, and a **release signing policy**. Add **GitHub Actions** as a trusted build system bound to `StruisICT/ClutterCutter`. 2. In the GitHub repo → **Settings → Secrets and variables → Actions**, add: - **Secret:** `SIGNPATH_API_TOKEN` - **Variables:** `ENABLE_SIGNING=true`, `SIGNPATH_ORGANIZATION_ID`, `SIGNPATH_PROJECT_SLUG`, `SIGNPATH_SIGNING_POLICY_SLUG`, `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`. 3. The next release's binaries are signed automatically. Until `ENABLE_SIGNING` is `true`, builds remain unsigned (and that's fine — winget doesn't require signing; it just reduces Defender/SmartScreen friction). ## Conventions - **Conventional Commits**, enforced on **PR titles** by `commit-lint.yml` (allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert). These drive the SemVer bump — see **Versioning (SemVer)** above (`feat:` → minor, `fix:` → patch, `feat!:`/`BREAKING CHANGE` → minor pre-1.0). - Work via PRs into `main` (history is all squash-merged PRs). - Keep the Rust tree `cargo fmt`-clean and `clippy`-clean. - Don't commit build artifacts (`*.exe`, `rust/target/` are gitignored). - Don't hand-edit `CHANGELOG.md` or version numbers — release-please owns them. ## Picking up on a fresh machine ```bash gh repo clone StruisICT/ClutterCutter cd ClutterCutter git log --oneline -15 # what shipped recently gh pr list # open release PR + any work in flight gh issue list # planned work ``` Then read this file, and continue in the Rust crate under `rust/`. ## Status & next steps _Last updated: 2026-08-18._ - Released: **v0.6.0**. A **Struis ICT visual redesign** landed after that (in PR #38, not yet released as its own version). It went through several passes: first the branded chrome, then a **1:1 match to the mockup** `index-selection.png` (light chrome; blue `#2D6BF0` drive/size accents; green `#70BB51` table bars; flat folder/file glyphs; two-line side-panel cards), then UX refinements: the top bar shows **Back/Forward/Up nav buttons** (history in `nav_hist`/`nav_pos`) instead of any logo/wordmark — **no Struis/ClutterCutter branding on the main screen; Struis ICT is named only in the About dialog**. The side panel has a **view-switch toolbar** (Top/Oldest/Temp icon buttons, active outlined in blue) and the theme toggle is a bordered **slider** pill. Theme changes force a synchronous chrome repaint (`RDW_UPDATENOW`). - The Rust build is the one and only app and matches the Struis ICT house style. The legacy C# implementation was removed from the repo in v0.9.2 (still recoverable from git history if ever needed). - A treemap view was built then **removed** at the user's request — don't re-add it without being asked. - The app persists its **window size** to `%APPDATA%\ClutterCutter\window.cfg` (saved on close in `WM_DESTROY`, restored in `run()` via `load_window_size`; skipped while maximized). Theme is not yet persisted. - Cross-platform egui GUI (`cluttercutter-gui`) + portable Linux scan core: **shipped** — drives, browse/largest/oldest, search, temp/caches, scan-any-folder, units toggle; builds and tests on Linux in CI (`linux.yml`). Remaining polish: native folder dialog (rfd), platform-appropriate system-cleanup actions on Linux/macOS, persisted egui settings. - Ideas / candidate next work (none committed yet — confirm before building): - Bulk-delete from the Temp-files view (currently per-row recycle). - Persist the theme choice too (window size already persists).