A lightweight, open-source desktop pet for Linux/Hyprland that renders an animated Pokémon companion reacting to what you're actually doing: typing, listening to music, or in a voice call. Frameless, transparent, always on top, pinned across workspaces, and hidden automatically when something goes truly fullscreen.
This repo ships no Pokémon artwork. src/main.ts and src/settings.ts
build sprite URLs at runtime against the community-maintained
PokeAPI sprite mirror and
pokeapi.co, the same way PokeAPI's own docs describe
for fan/hobby projects. Only this repository's own code is distributed;
images are fetched live and cached by the webview's normal HTTP cache. If
you'd rather use your own art (or a licensed sprite pack), swap
src/sprites.ts for your own URL scheme or bundle local files.
┌─────────────────────────┐ pet://state / pet://config
│ Rust backend (Tauri) │ ───────────────────────────────────┐
│ │ │
│ input_evdev.rs ── reads /dev/input/event* directly │
│ media.rs ── polls playerctl (MPRIS) │
│ voicechat.rs ── polls pactl source-outputs (PipeWire) │
│ hyprland.rs ── polls hyprctl for true-fullscreen hide │
│ state.rs ── priority state machine: vc > typing > │
│ music > wander/sleep │
│ tray.rs ── tray icon + right-click context menu │
└─────────────────────────┘ │
▼
┌───────────────────────────────────────────────────────────────┐
│ Frontend (TypeScript, no framework) │
│ pet.html/main.ts — the pet window: renders sprite state, │
│ drags the OS window around to "walk" │
│ settings.html/settings.ts — species picker + behavior toggles │
└───────────────────────────────────────────────────────────────┘
Everything runs as a single Tauri binary — the "system polling daemon" is
just async Rust tasks inside the same process, which keeps installation to
one binary. The module boundaries (input_evdev.rs, media.rs,
voicechat.rs) are intentionally decoupled from Tauri specifics if you ever
want to split them into a standalone daemon later.
Highest wins, each tick: voice chat (pactl capture stream from a known VC app) → typing (evdev keyboard press within the last ~1.8s) → music (playerctl reports "Playing") → sleep (no input for the configured idle window, or wandering disabled) → wander (default).
- Linux, Wayland (developed against Hyprland; degrades gracefully elsewhere)
playerctl, PipeWire'spactlcompat layer- Node.js + npm, Rust + Cargo
- Your user in the
inputgroup, to read/dev/input/event*for typing detection (see below — this is the one privileged step)
./scripts/setup.sh # checks deps, runs npm install, prints next stepsThe script will tell you if you're not in the input group yet:
sudo usermod -aG input $USER
# log out and back in for this to take effectWithout this, typing detection silently does nothing (permission denied opening the event devices) — everything else still works.
npm run tauri dev # dev, hot-reloading frontend
npm run tauri build # release binary -> src-tauri/target/release/cyberpetecho 'source = ~/Projects/cyberpet/hypr/cyberpet.conf' >> ~/.config/hypr/hyprland.confSee hypr/cyberpet.conf for the window rules (float, pin, no border/shadow/
blur/anim, no focus stealing) and an exec-once line to autostart the built
binary. Double-check the window class with hyprctl clients | grep -i cyberpet and adjust the regex in that file if your build reports a
different one.
Fullscreen-hide is enforced twice: once by Hyprland's own pinned-window
behavior, and again explicitly by src-tauri/src/hyprland.rs, which polls
hyprctl activewindow -j and hides the pet window whenever the focused
window is truly fullscreen. This means the feature works even if a given
Hyprland version's default pinned-window/fullscreen interaction changes.
- Left-click drag the pet to reposition it manually.
- Right-click (or the tray icon) opens the same menu: switch behaviors on/off, or open Settings.
- Settings window: pick any of the first 151 species (extend the limit
in
src/settings.ts/fetchPokemonListfor later generations), tune walk speed, scale, and idle-to-sleep timing. - Voice-chat mode: when a known VC app (Discord, WebCord, Vesktop,
Mumble, Teams, TeamSpeak — edit
vcAppsin Settings' saved config orconfig.rs'sConfig::default()) has an active PipeWire capture stream, the pet puts on a pixel headset and shows a clickable speech bubble (click it to cycle flavor text). This is a heuristic — it detects an open mic stream, not real voice-activity — seevoicechat.rs.
Config persists to ~/.config/cyberpet/config.toml.
Colors/fonts match the "CyberArch" rice this was built alongside: accent red
#FF2D3D, near-black translucent panels, Tektur for headers, Quantico
for labels, FiraCode Nerd Font for monospace bits. Edit src/style.css and
src/settings.css directly — there's no theming abstraction layer, just CSS.
App crashes instantly with Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display. — this is a known WebKitGTK/NVIDIA-proprietary-driver
incompatibility (its DMA-BUF compositing path isn't supported there), not a
bug in this app. main.rs already sets WEBKIT_DISABLE_DMABUF_RENDERER=1
before the webview is created to work around it; if you still hit it on some
other GPU/driver combo, try also exporting WEBKIT_DISABLE_COMPOSITING_MODE=1
before launching.
- Typing/idle detection only sees devices present at startup (no hotplug handling yet).
- Voice-chat detection is a mic-stream heuristic, not true VAD.
- Only one "facing" sprite frame is available (animated PokeAPI sprites are front-only); walking left/right is a CSS horizontal flip, not a real walk-cycle sprite sheet.