Animated flies that swarm your Trash / Recycle Bin as it fills up. Free giveaway app for macOS and Windows. Direct download only.
Cross-platform Qt 6.11 + C++. Separate from Hypershot β its own repo, its own signing identity, its own updater feed.
macOS works end to end: the overlay sits above the Dock, tracks the real Trash icon through magnification, and the swarm grows with the item count.
Windows works too β RecycleBinTarget finds the bin through the
desktop's shell view, reads count and size from SHQueryRecycleBin,
and the overlay hides itself whenever a window covers the desktop at the
bin. The icon rect is calibrated by measurement rather than by eye:
binprobe --measure matches the shell's own artwork against the screen
and prints where the icon really is. See docs/windows-backend.md.
See docs/architecture.md for the decisions and docs/battery.md for
the power budget.
macOS / Linux:
sh scripts/install-hooks.sh # once per clone β see Secrets below
cmake --preset dev && cmake --build --preset devWindows β the presets are deliberately disabled here (cmake --preset dev
answers "Could not use disabled preset"). Build from a Developer Command
Prompt for VS, which is what puts cl.exe on PATH:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build buildNinja ships with Qt at C:\Qt\Tools\Ninja if it isn't already on PATH.
In Qt Creator on Windows, use a normal kit β Desktop Qt 6.11.1 MSVC2022
64bit β not a "(CMake preset)" build configuration. Qt Creator derives a
kit from every enabled preset, and a preset that names no compiler yields
a kit with no toolchain and no Qt: it then configures with the NMake
generator outside the MSVC environment and dies on nmake -? / "no such
file or directory". Naming the compiler in the preset doesn't help β Qt
Creator matches a toolchain by absolute cl.exe path, which is
machine-specific and can't be committed β so the presets simply stop at
the Windows boundary and leave kit selection to the IDE. This is how
Hypershot builds on both platforms too.
Qt's location is never committed. The build auto-detects the newest
~/Qt/6.* install; override with QT_ROOT, with
-DCMAKE_PREFIX_PATH=..., or by pinning it in a CMakeUserPresets.json
(git-ignored) that inherits dev.
An empty -DCMAKE_PREFIX_PATH= is deliberately treated as "not
specified" rather than "search nowhere": an editor that expands an unset
$env{} to an empty string would otherwise silently break the Qt lookup,
which is exactly how this broke once already.
This repo is public. gitleaks runs in two places:
- Pre-commit hook (
.githooks/pre-commit) β the one that actually prevents a leak, since CI only fires after a push has already made the secret public. Enable per clone withscripts/install-hooks.sh. - CI (
.github/workflows/secret-scan.yml) β full history on every PR, plus weekly, so secrets missed by an older ruleset still surface.
If something is caught, rotate the credential first. Removing it from the diff does nothing for a value that was already pushed β it stays in the history and must be treated as compromised.
macOS is live β DockTrashTarget reads your real Trash and finds the
real Dock icon. Windows still returns the stub.
Two permissions, and neither is Full Disk Access. Both are asked for by the app itself β no console-only failures:
- Accessibility β the only way to get Dock item geometry. If it's missing the app shows the system prompt (which carries an "Open System Settings" button), then watches once a second and starts tracking the moment it's granted. No restart needed.
- Automation (Finder) β for the item count. Reading
~/.Trashdirectly is TCC-protected and would need Full Disk Access; asking Finder needs only an Automation prompt, a far smaller ask for a free app. If FDA happens to be granted we use the direct read instead.
openRemediation() opens whichever settings pane is the blocker, for
when the one-shot system prompt has already been dismissed.
The build MUST be signed with a stable identity β the CMake post-build step does this with the Developer ID. Without it the bundle is ad-hoc / linker-signed, and TCC identifies ad-hoc apps by their code hash, which changes on every rebuild. Every rebuild then looks like a brand new app, the Accessibility grant silently evaporates, and the app re-prompts forever. Verify with:
codesign -d -r- build/hyperbin.app # must name the identifier + team,
# not be adhocSet -DHYPERBIN_CODESIGN_IDENTITY= to skip signing (CI without certs);
expect the re-prompt behaviour if you do.
Dev gotcha: launching from a terminal that itself has Accessibility
makes the terminal the responsible process, so the app inherits the grant
and you can never reach the ungranted path. HYPERBIN_FORCE_NO_AX=1
forces it, which is how the prompt flow gets tested.
Windowed (use this one). A manual sim playground: normal window, a drawn stand-in for the bin, and a slider that owns fullness. Nothing else drives it, so you can hold a value and watch the swarm settle.
./build/hyperbin.app/Contents/MacOS/hyperbin --windowedDrag the slider 0 β 100%: one lonely fly at the low end, up to 10 at full, agitation rising with it. Drag back to 0 and they should disperse outward and disappear entirely β if any remain, the render clock never stops and the power budget is broken.
Overlay. The real thing: frameless, click-through, above the Dock, on every Space, tracking your actual Trash.
./build/hyperbin.app/Contents/MacOS/hyperbin
HYPERBIN_DEBUG=1 ./build/hyperbin.app/Contents/MacOS/hyperbin # geometry log
./build/hyperbin.app/Contents/MacOS/hyperbin --paintdebug # fill the overlayNothing draws when the Dock is auto-hidden β the icon is parked outside every display and there is nothing to sit on. Turn auto-hide off to see it, or reveal the Dock and wait up to a second for the poll to notice.
Headless. ./build/simtest β asserts the swarm populates, moves,
stays over the bin, scales with it, and fully disperses when emptied.
VS Code: .vscode/launch.json has all three as debug configs.
- Windows has no backend yet β still the stub.
- The mask lags the Dock by a frame. Flies pass behind the bin via a
shader that erases them where the Trash artwork is opaque
(
shaders/flymask.*). Its position comes from the Accessibility poll, which is one frame behind during Dock magnification, so flies are hidden a few pixels off. Barely perceptible β and far better than the copy-the-icon approach it replaced, where the same lag showed as a visibly desynced duplicate bin. - Only
~/.Trash. Items trashed from other volumes live in/Volumes/<x>/.Trashes/<uid>and aren't counted. - Polling, not notifications. The icon is polled at 50ms while on
screen (fast enough to follow Dock magnification) and 1s when hidden,
so revealing an auto-hidden Dock lags up to a second. An
AXObserveron the Dock item would make it immediate and cheaper. - No menu-bar item, no onboarding. Both permissions currently have to be granted by hand; nothing explains them to the user yet.
- Power is unmeasured. 60fps was chosen for looks over the original
20fps budget;
docs/battery.mdhas the numbers that need verifying.
src/core/ platform-agnostic: fly simulation, bin state, settings
src/platform/ the ~20% that differs: TrashTarget + OverlaySurface impls
src/render/ QSGGeometryNode instanced sprite renderer
qml/ tray UI + onboarding (not the flies β those are scene graph)
Fly motion is emergent, not authored. Steering behaviours (wander + separation + containment) produce better-looking flies in ~200 lines than hand-keyframed loops would, and they respond continuously to bin fullness instead of switching between discrete states. Rive earns its place when a designer needs to author the motion; here the motion is a simulation. Revisit if we add authored character animation.
The simulation is also what makes the power budget achievable β see below.