Dhruv Kumar

nekopet

A desktop pet for Wayland, in Rust. Speaks wlr-layer-shell directly, reads raw key events from /dev/input, renders through shared-memory buffers. Its eyes follow the cursor.

RoleOpen source, solo buildStackRust, wayland-client, smithay-client-toolkitSourcegithub ↗

The problem

I wanted a desktop pet on Hyprland, the kind that used to live on Windows desktops years ago. Wayland has no equivalent of an always-on-top, click-through, input-transparent window that a compositor just hands you. Getting that overlay right meant talking to the compositor’s own protocols, not a toolkit abstraction built for normal application windows.

The harder problem showed up once the overlay worked: a surface that correctly asks for no keyboard interactivity also never gets told when a key is pressed. There was no protocol-legal way to know the user was typing.

How it works

01Sit on top, take nothingA wlr-layer-shell surface on the top layer, with keyboard interactivity explicitly set to none. It draws over every window but never steals a keystroke or blocks a click on what is behind it.
02One sprite sheet, seven statesIdle, typing, drag, stretch, bounce, lean, and a cursor-swipe pounce all read from a single 32x32-per-frame sheet, one row per animation, frames stopping at the first fully transparent cell. Nearest-neighbor upscaling keeps the pixel art sharp at any size.
03Detect typing without a keyboardA layer surface with no keyboard interactivity never receives wl_keyboard events, so there is no protocol-level way to know the user is typing. Nekopet opens every /dev/input/eventN device directly and parses the raw 24-byte input_event struct itself, watching for key-press codes on its own thread per device.
04Eyes that follow the cursorWayland gives a client no way to query the pointer position outside its own surface, so tracking only works while the cursor is over the cat. Pupil offsets are computed from pointer-motion events against hardcoded source-pixel coordinates on the sprite.

Result

1,000Lines of Rust for input, rendering, and the Wayland client
7States driven from one event loop, no animation library
0External processes, no Python, no Electron, one native binary

What I would change next

The keyboard watcher opens every /dev/input/eventN node it can and spawns a thread per device instead of filtering to actual keyboards first. It works because a typical machine only has a handful of event nodes, but it is the kind of shortcut that stops being fine on a machine with more input devices attached.