Sitelet https://github.com/EroyEroy/code-duck
Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CodeDuck

A rubber duck that sits above your taskbar and reacts to how you code.

CI Latest release Downloads Windows License: MIT

CodeDuck sitting above the Windows taskbar

A desk companion aimed at programmers: the duck taps a wing per keystroke, sparks up when you hit a flow state, gets confused when you spam backspace, and dozes off when you stop. Its eyes follow your cursor, it blinks, and it is docked to the taskbar — drag it along, but not off.

It reacts to typing in any application, not just its own window. That is the whole point, and it is also why the next section is the longest one.

What it does

When you The duck
press a key taps one wing — one key, one tap
type fast for more than 5 seconds sparks up (flow)
hit a run of backspaces gets confused
stop for 90 seconds falls asleep, zZz
move the mouse follows it with its pupils

Right-click the tray icon to hide the duck, reset its position, or quit.

Privacy: yes, this reads your keyboard. Here is exactly what it does.

The duck reacts to typing in your editor, not in its own window. On any OS that requires a system-wide keyboard hook — mechanically the same thing a keylogger installs. Pretending otherwise would be dishonest, so instead the design makes the dangerous part small and auditable.

Nothing about your typing is stored. Anywhere. Ever. Not to disk, not in memory beyond a second or so. Keystrokes drive the duck's mood and are then gone. The only thing this app persists is where you dragged the duck to.

The raw keycode lives for exactly one function call. src/main/input/classifier.ts is the only module in the app that ever sees one. It reduces the keycode to one of five categories and returns that:

type | newline | delete | nav | modifier

The keycode itself is never stored, never logged, never sent over IPC, and never written to disk. The renderer — the part that could in principle talk to a network — is architecturally incapable of learning which key you pressed, because that information does not survive the trip.

Five buckets is enough to drive every reaction the duck has, and far too coarse to reconstruct text. a, b and 7 are indistinguishable once classified.

This is enforced, not just promised:

  • classifier.test.ts sweeps every possible keycode and asserts the output is always one of the five categories.
  • One test replays a real recording off the global hook — someone typing "hello duck", backspacing, pressing Enter, then Ctrl+S — and asserts the ten letters collapse into ten identical values. The raw stream spells the words out; the classified stream cannot.
  • ESLint forbids src/shared/** from importing Electron or Node APIs, so the code shared with the renderer cannot reach for a filesystem or a socket.

The mouse gets the same treatment. The duck's eyes follow your cursor, which means main watches the pointer globally too. It does not pass the position on: it computes a direction from the duck's centre and sends that unit vector. The renderer aims the pupils without ever learning where your mouse is, and nothing about the cursor is written to disk. Same rule as keystrokes — send the least that does the job.

No network calls of any kind. The overlay's CSP blocks remote origins outright. There is no telemetry, no update check, no analytics.

Install

Windows 10 or 11, 64-bit. Grab the installer from Releases.

The build is unsigned, and SmartScreen will warn you on first run. Click "More info" → "Run anyway". This is expected: an app that installs a global keyboard hook without a code-signing certificate is exactly the shape of thing SmartScreen and antivirus heuristics are built to flag. That is the honest cost of the feature — a certificate is out of scope for now, and the source is right here if you would rather build it yourself.

Why Windows only

The duck is docked to the Windows taskbar, and that is baked in deeper than a build target:

  • setAlwaysOnTop(true, 'screen-saver') is the level that floats above the taskbar; plain always-on-top sits below it.
  • screen.screenToDipPoint, used to reconcile the hook's physical pixels with the window's DIPs, is a Windows-only Electron API.
  • The e2e suite runs on windows-latest because that is the platform the overlay actually targets.

macOS and Linux are not built, not tested, and not promised. On macOS the global hook would need Accessibility permission and Gatekeeper would block an unsigned app; on Linux uiohook-napi needs X11 and would go deaf under Wayland. Those are real ports, not a config flag.

Build from source

corepack enable
pnpm install
pnpm dev

The duck appears bottom-right, sitting on the taskbar. Drag it along the taskbar — it is docked, so it slides sideways and will not float off; the spot is remembered.

Command Does
pnpm dev Run in development with hot reload
pnpm build Build all three processes
pnpm test Unit tests
pnpm typecheck Typecheck main/preload and renderer
pnpm lint ESLint
pnpm format Prettier
pnpm test:e2e Playwright against the real built app
pnpm preview Compose the layers into one image per state
pnpm exec electron-builder --win Build the installer into release/

Tagging v* and pushing builds the installer on a Windows runner and publishes it to Releases — see .github/workflows/release.yml.

Stack

Electron · React · TypeScript · Vite (electron-vite) · pnpm via corepack · Vitest · ESLint + Prettier · husky + commitlint · GitHub Actions

Architecture

Three processes, with main as the source of truth:

uiohook keyboard (main)
  -> classifier.ts     keycode -> category, keycode goes no further
  -> 50ms batching     coalesced so key-repeat cannot flood IPC
  -> IPC 'input:batch' -> preload (contextBridge) -> renderer
                          -> companion state machine -> duck layers

uiohook mouse (main)
  -> hit test          over the duck? -> setIgnoreMouseEvents
  -> drag              slides along the taskbar; Y is always derived
  -> gaze              direction only -> IPC -> pupils
  -> on drop           window position -> electron-store

Both global hooks live in main, and so does the only thing that persists: where the duck sits. Nothing counts or stores the batches themselves.

Everything the pointer does lives in main too, off the same global hook. That is not incidental: the renderer cannot reliably hit-test a click-through window, and the drag region it needed for dragging was what stopped the events arriving. See src/main/input/pointer.ts.

src/shared/ is typechecked twice — once under the Node config, once under the web config — which mechanically keeps it free of both Node-only and DOM-only APIs. classifier and the companion state machine are pure modules with no Electron import, so they unit-test without booting the app. The machine is a reducer over (state, event), which makes "asleep after 90 seconds" an instant test rather than a 90-second wait.

Art

The duck is pixel art, composed from layered parts rather than sprite sheets: body, wings, pupils, keyboard and effects are separate images stacked and moved independently. Pupils have to be their own layer to follow the cursor at all, and single parts are also the only thing an image model can produce consistently — strips of aligned frames are not.

Everything moves in whole art pixels. Fractional offsets and rotation resample the grid and turn pixel art to mush, so layers only ever translate by integers or mirror with scaleX(-1) (an exact flip). Effects animate with CSS steps() for the same reason, and animation stays on the compositor — an overlay that is visible all day repainting from JS every frame would burn CPU for nothing.

Geometry and palette live in src/shared/duck-art.json, read by the app and pnpm preview, so the two cannot drift apart.

Typing animation is event-driven: the state machine counts keystroke batches and its parity picks which wing taps next — one keystroke, one tap. Nothing loops on a timer. Blinking works the same way: the lid and the pupils share one CSS timeline with opposite opacity, so no state and no timer are involved in either.

assets/duck/*.png is the art — each sprite drawn by hand at exactly the size its rect gives, with no import step and no pipeline. pnpm preview stacks the layers into one image per state, so a misplaced part is obvious without launching anything.

There was an importer that downscaled generated art automatically. It is gone: averaging is a photo algorithm and cannot preserve a 1px outline, so it turned the duck's dark edge into patchy orange. Drawing at final size means there is nothing to resample at all.

Licence

MIT

About

A rubber duck that sits above your Windows taskbar and reacts to how you code: one wing tap per keystroke, sparks in a flow state, asleep when you stop. Reads your keyboard globally and stores nothing.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages