A tiny Windows tray utility that rotates the mouse cursor to follow your movement direction — like the aiming arrow in Worms 3D. Move the mouse right and the arrow points right; sweep it down and it smoothly slews to point down. The link/hand cursor rotates the same way.
It runs in the background from the system tray, with no main window.
⚠️ It replaces the system-wide arrow cursor (SetSystemCursor) while running, so every app sees the rotated arrow. The defaults are restored on quit and on a crash, and WormsCursor reloads the real cursor scheme on every launch — so if it's ever killed outright (Task Manager "End Task", Visual Studio "Stop Debugging") and the cursor stays rotated, just starting it again clears it. To fix it without relaunching, runtools/RestoreCursor.ps1(logging off resets it too).
video.mp4
The cursor rotating to follow your mouse movement.
WormsCursor themes all 14 standard system cursors, each with its own physics-driven motion:
Generated by the WormsCursor.Preview tool:
dotnet run --project src/WormsCursor.Preview -- assets/cursors.png (writes a dark sheet
plus a transparent one).
- At startup the engine pre-renders 360 rotated copies of the arrow (one per degree) as cursors, each with the hotspot pinned to the arrow tip.
- A background loop polls the cursor position (144 Hz by default) and computes the movement direction from accumulated travel, so ±1 px jitter doesn't make the arrow wobble on straight or vertical moves.
- The displayed angle is animated toward the target direction (a fixed deg/s slew), so turns look smooth instead of snapping.
- Both the arrow (
OCR_NORMAL) and the hand/link cursor (OCR_HAND) rotate. The hand is drawn the same way as the arrow — a filled silhouette plus a pen-stroked outline, finger separators and knuckle creases — so it shares the arrow's colour, size and outline thickness (the geometry is baked intoHandShape.cs). - The busy / progress cursors are themed too. App-starting (
OCR_APPSTARTING) is the rotating arrow with a spinning ring of dots that hangs off its tail like a pendulum — swinging out as you move and settling when you stop — while wait (OCR_WAIT) is the same ring centred on the pointer (spin only). They animate only while actually on screen, so an idle tray costs nothing. - The help cursor (
OCR_HELP) hangs a "?" upside-down off the arrow's tail on the same string, swinging to the sides as you move and settling when you stop. - The crosshair (
OCR_CROSS) is a precision reticle — a centre dot, four ticks and a slowly-rotating broken ring; the ticks breathe and spread out (recoil) when you move fast, then settle. - The text / I-beam cursor (
OCR_IBEAM) is a flexible beam: the bottom stays rigid while the top sways opposite to your motion on a soft spring, wobbling like jelly and settling. - The resize cursors (
OCR_SIZEWE/SIZENS/SIZENWSE/SIZENESW) and the move cursor (OCR_SIZEALL) are stretched-taffy double-arrows — drag along an axis and the shaft necks thin while the heads fly apart on a spring, then blob back; move crosses a horizontal and a vertical taffy into a 4-way glyph. - The unavailable cursor (
OCR_NO) is a red circle-with-slash whose ring is a jelly blob: it deforms into an egg along the direction of travel and wobbles back to round. - Alternate-select (
OCR_UP) reuses the same rotating arrow. - The animated cursors re-render only while actually on screen (matched via
GetCursorInfoagainst the live system handle), so an idle tray uses no CPU.
When an AI coding agent needs your attention, the cursor sprouts a small logo charm that hangs and swings on the same pendulum as the busy ring / help "?" — one logo for the waiting tool, with a frameless "+N" when several agents wait at once. It's an ambient, peripheral-vision nudge: no toast, no extra window. When nothing's waiting it costs nothing — the cursor falls back to its plain pre-rendered frames.
Set it up from Preferences… → Agent settings…: register a tool there (currently Claude
Code), toggle the logo, and set the "clear a stuck logo after" timeout. Registering writes
WormsCursor's hook command into the tool's config (~/.claude/settings.json) with
backup-and-merge — it never overwrites your own hooks. Each event then runs
WormsCursor.exe hook --tool …, a throwaway process that writes one line to the running tray app
over a named pipe and exits. It's fail-silent (<½ s pipe timeout, errors logged to
bridge.log, always exits 0), so it can never block or break the agent.
What it reacts to. Each tool's lifecycle hooks are normalised to a few events, and the logo tracks whether the ball is in your court:
| The agent… | Claude hook | Logo |
|---|---|---|
| is blocked on you (permission / idle prompt) | Notification |
appears |
| finished its turn | Stop |
appears |
| ended on an error | StopFailure |
appears |
| started on your prompt, or you reopened the session | UserPromptSubmit / SessionStart |
clears |
exited cleanly (/exit, Ctrl+C, /clear, logout) |
SessionEnd |
clears at once |
So it shows up when an agent is waiting on you and clears the moment you're clearly back (you submitted a prompt) or the session ends. Multiple sessions are tracked independently (keyed by tool
- session id), so the count is simply "how many agents need you right now".
Why it's event-driven (and the timeout). WormsCursor only knows what the hooks tell it over the
pipe — it never polls the agent or watches its process. That keeps it dead-simple and zero-cost when
idle, but it means a session that never sends a closing event would otherwise wait forever. So
there's a backstop: any waiting session with no further events for the linger timeout (default
60 s, configurable 30–1800 s) is swept out of the count. The tool-call hooks (PreToolUse /
PostToolUse) are deliberately not registered — they'd spawn a hook process on every single tool
call — so "you replied" is inferred from your next prompt, not from the agent resuming work.
- Non-standard cursors (grab / grabbing / zoom-in / zoom-out / cell) aren't themed and fall
back to the Windows defaults. These aren't Windows system cursors — there's no
OCR_*slot for them, soSetSystemCursor(how WormsCursor themes everything) has nothing to replace. They're CSS cursor values that the application draws itself: a browser handlesWM_SETCURSORand callsSetCursorwith its own embedded cursor resource, bypassing the global system-cursor table entirely — so nothing we install there is ever seen. Any way to override them would necessarily be a hack: an owner-drawn overlay that hides the real cursor and paints our own (the same rearchitecture the mixed-DPI flicker would need), plus capturing and re-styling whatever bitmap the app set, since the handle we read back can't be reliably told apart (grab vs zoom vs cell). Outside a browser these cursors essentially never appear. - Animated cursors flicker on a mixed-DPI multi-monitor setup. If your monitors run at
different display scales (e.g. 125% on one, 150% on another), the animated cursors
(busy/wait, app-starting, help, crosshair, …) can strobe while shown on the higher-DPI
screen. They animate by re-installing the cursor via
SetSystemCursorevery frame, and on a monitor whose scale differs from the one the cursor was created for, Windows re-composites the global cursor on each swap — which flashes. The static cursors (the rotating arrow/hand at rest) are re-installed only on a direction change, so they don't flicker. Single-DPI setups and matching scales are unaffected. Workarounds: set both monitors to the same scale, or enable Pointer trails (Settings → Bluetooth & devices → Mouse → Additional mouse settings → Pointer Options, shortest setting), which forces a cursor-draw path that sidesteps the flicker. A proper fix would mean drawing the animated cursors in our own overlay instead of throughSetSystemCursor(tracked for a future release). - A hard-killed agent's logo lingers until the timeout. The agent notifier learns everything
from the tool's hooks over the pipe — it never watches the agent's process. A clean exit
(
/exit,Ctrl+C,/clear, logout) firesSessionEndand the logo clears immediately, but if the agent is killed outright (Task Manager "End Task", closing the terminal window,kill) no hook runs, so no "session ended" ever reaches WormsCursor. The waiting logo then stays until the linger timeout sweeps it (default 60 s, set in Agent settings…). This is by design — with no event there's nothing to react to, and the timeout is the backstop.
WormsCursor.sln
├─ src/
│ ├─ WormsCursor.Core/ Engine — no UI dependencies
│ │ ├─ CursorEngine.cs P/Invoke, cursor building, tracking + animation loop
│ │ ├─ ArrowRenderer.cs Draws the arrow (size, colours, thickness, corner radius)
│ │ ├─ HandRenderer.cs Draws the hand/link cursor (solid fill + baked line art)
│ │ ├─ ProgressRenderer.cs Draws the composited cursors (busy, help, crosshair, text, resize/move, unavailable)
│ │ ├─ HandShape.cs Baked hand geometry (silhouette + crease marks)
│ │ ├─ NotifierRenderer.cs Composites the agent-notifier logo charm onto a cursor
│ │ ├─ AgentLogos.cs Baked tool logos (Claude critter, OpenAI knot); SvgPath.cs parses the path data
│ │ ├─ AgentActivity.cs Tracks which agent sessions need you (UI-free, thread-safe); AgentEventMessage.cs is the wire format
│ │ ├─ CursorSettings.cs Tunable parameters (persisted as JSON)
│ │ └─ SettingsStore.cs Load/save settings in %LocalAppData%\WormsCursor\
│ ├─ WormsCursor.App/ Tray shell (WinForms, no main window)
│ │ ├─ Program.cs Entry point (Velopack + single-instance guard; `hook` verb short-circuits first)
│ │ ├─ TrayApplicationContext.cs NotifyIcon + menu, owns the engine, pipe server + keyboard hook
│ │ ├─ PreferencesForm.cs Live settings dialog (preview grid, size/colour/thickness/radius, feedback toggles, test cursor + Showtime)
│ │ ├─ AgentHooksForm.cs Agent-notifier settings + per-tool hook registration UI
│ │ ├─ AgentPipeServer.cs Named-pipe server that receives normalised agent events
│ │ ├─ AgentHookBridge.cs The `hook` verb: normalises a tool's payload → one pipe line, fail-silent
│ │ ├─ Services/AgentHookRegistrar.cs Writes/removes WormsCursor's hook in each tool's config (backup + merge)
│ │ ├─ LowLevelKeyboardHook.cs Keyboard hook driving the I-beam typing bounce
│ │ ├─ SingleInstance.cs One instance only; a 2nd launch opens Preferences
│ │ ├─ Autostart.cs "Start with Windows" via HKCU\…\Run
│ │ ├─ ChangelogForm.cs "What's new" dialog (from CHANGELOG.md)
│ │ └─ Services/UpdateService.cs Velopack check / download / apply updates
│ └─ WormsCursor.Preview/ Console tool — renders the cursor showcase PNGs (docs/README)
│ └─ Program.cs Dark + transparent sheets of every themed cursor
└─ tools/
├─ generate-icon.py Builds Assets/Icon.ico (+icon.png) — the arrow glyph
└─ RestoreCursor.ps1 Emergency restore of default cursors
The app/tray icon is the same arrow as a white glyph with a black frame (so it reads
on both dark and light taskbars), generated by tools/generate-icon.py (Pillow).
Regenerate after tweaking the shape/angle:
python tools/generate-icon.pyThe engine (Core) is deliberately UI-agnostic, so the tray shell could later be
swapped for WPF/WinUI — or packaged for the Microsoft Store — without touching the
cursor logic.
Requires the .NET 8 SDK (Windows). Open WormsCursor.sln in Visual Studio 2022+
and run, or from a terminal:
dotnet build WormsCursor.sln
dotnet run --project src/WormsCursor.AppA tray icon appears. Right-click it for Enabled / Preferences… / Exit; double-click toggles the effect on/off. Only one instance runs — launching it again just opens Preferences.
Preferences… opens a live editor with a preview grid of all the cursors — hover a
tile to try it on the real pointer, or untick it to keep the Windows default. It has sliders
for size, outline thickness and corner radius, fill and outline colour
pickers, click-feedback and I-beam typing-bounce toggles, a Test cursor picker
(plus Showtime, a hands-free cycle through the enabled cursors for recording), a Start
with Windows toggle, an Agent settings… button, and a Check for updates button.
"Start with Windows" registers a per-user HKCU\…\Run entry (no admin), which you can also
manage from Task Manager → Startup apps. Settings are saved to
%LocalAppData%\WormsCursor\settings.json and persist across restarts — and across updates,
since they live outside the app folder.
dotnet publish src/WormsCursor.App -c Release -r win-x64 --self-contained true -p:PublishSingleFile=trueNote: WinForms doesn't support trimming/NativeAOT, so a self-contained build is several tens of MB. A framework-dependent build is tiny but needs the .NET 8 Desktop Runtime installed.
Installers and standalone builds are produced by Velopack:
pushing a v* tag runs the release workflow (.github/workflows/release.yml), which
publishes a Setup.exe installer, a Portable.zip standalone, and delta
packages to the repo's GitHub Releases. The app auto-updates from these releases
(tray → Check for updates…, or the button in Preferences).
MIT © 2026 Dawid Wenderski

