03 / Open source · solo build
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.
- Role
- Open source, solo build
- Stack
- Rust, wayland-client, smithay-client-toolkit
- Source
- github
01The 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.
02How it works
Sit on top, take nothing
A 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.
One sprite sheet, seven states
Idle, 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.
Detect typing without a keyboard
A 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.
Eyes that follow the cursor
Wayland 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.
03Result
- Lines of Rust for input, rendering, and the Wayland client
- 1,000
- States driven from one event loop, no animation library
- 7
- External processes, no Python, no Electron, one native binary
- 0
04What 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.