[Detail Bug] Cat plugin: decorative cat stops wandering when app window loses focus (alt-tab/minimize)

Open Beginner friendly
#87 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
Under an hour
Newbie friendliness
90/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
desktop

Research direction

Start in apps/knx-gui/src/knx_gui/plugins/cat/follower.py at CatFollower.render(), then reproduce the behavior by alt-tabbing or minimizing the application while the cat is wandering. Verify that an invalid unfocused-window mouse position no longer cancels wandering and that normal mouse movement still triggers the sprint back to the corner.

Written by the indexing model from the issue text.

Description

Detail Bug Report

https://app.detail.dev/org_62aa40f5-2c23-4914-a665-3bb2068af20e/bugs/bug_5bb67883-0bde-4cba-abdb-60af32f757bf

Introduced in 187848f4163973ac8cc27ec8c31301b58af25e72 by @kewde on May 17, 2026

Summary

  • Context: CatFollower (apps/knx-gui/src/knx_gui/plugins/cat/follower.py) is the decorative-cat plugin. docs/plugin.md lists cat as the only plugin with no panels and no service — "Cosmetic desktop cat follower." A sprite sits at the bottom-left corner, wanders to a random spot along the bottom every 3–8 s of idle time, and sprints back to the corner when the mouse moves. The idle-wander feature was introduced by commit 187848f ("refactor(cat): replace click-following with idle wander behavior"): "Cat now wanders slowly when user is away and sprints back to corner when mouse movement is detected."
  • Bug: mouse_moved is computed from io.mouse_pos before the validity guard on the next line that rejects Dear ImGui's "no mouse" sentinel (-FLT_MAX, -FLT_MAX), so when the mouse position is the sentinel, mouse_moved is True on every frame and the spook-to-corner branch fires the frame after every wander attempt — aborting the wander. The actual trigger is the application window losing focus (alt-tab to another application, or the window being minimized / obscured): Dear ImGui's GLFW backend sets io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX) each frame when the window is unfocused.
  • Actual vs. expected: When the window is unfocused, the cat remains pinned within ~_WANDER_DIST of the corner (wander attempts are immediately cancelled). Expected: the cat continues the normal idle-wander behavior.
  • Impact: Low / cosmetic. While the app window is unfocused, the decorative cat does not roam along the bottom as intended.

Code with Bug

apps/knx-gui/src/knx_gui/plugins/cat/follower.py, CatFollower.render():

        io = imgui.get_io()
        mx, my = io.mouse_pos.x, io.mouse_pos.y
        mouse_moved = (
            abs(mx - self._last_mouse_x) > _MOUSE_MOVE_THRESHOLD
            or abs(my - self._last_mouse_y) > _MOUSE_MOVE_THRESHOLD
        )  # <-- BUG 🔴 computed before mouse-pos validity check; -FLT_MAX sentinel makes this True every frame
        if abs(mx) < 100_000 and abs(my) < 100_000:
            self._last_mouse_x, self._last_mouse_y = mx, my

Spook branch that then cancels wandering:

        if mouse_moved and (self._wandering or self._returning) and not self._sprinting:
            self._sprinting = True
            self._wandering = False
            self._returning = False
            self._target_x = corner_x
            self._target_y = ground_y

Explanation

  • When the Dear ImGui GLFW backend reports no mouse (window unfocused), it sets io.mouse_pos to (-FLT_MAX, -FLT_MAX).
  • The code correctly avoids saving this sentinel into _last_mouse_x/_last_mouse_y, so those values remain frozen at the last real cursor position.
  • However, mouse_moved is computed before the validity guard. With (mx, my) == (-FLT_MAX, -FLT_MAX) and _last_mouse_* frozen, abs(mx - last) > _MOUSE_MOVE_THRESHOLD is True every frame.
  • As a result, on any frame after wandering starts (self._wandering=True), the spook branch triggers immediately and retargets the cat back to the corner, effectively preventing any visible wander while unfocused.

Codebase Inconsistency

In commit 187848f, the old logic early-returned on invalid mouse values (if abs(mx) > 100_000 or abs(my) > 100_000: return), preventing the sentinel from affecting state. The refactor removed the early return and introduced the new ordering where mouse_moved is computed from unvalidated values.

Recommended Fix

Compute mouse_moved only when the mouse position is valid (reuse the existing validity check):

        _valid = abs(mx) < 100_000 and abs(my) < 100_000
        mouse_moved = _valid and (
            abs(mx - self._last_mouse_x) > _MOUSE_MOVE_THRESHOLD
            or abs(my - self._last_mouse_y) > _MOUSE_MOVE_THRESHOLD
        )
        if _valid:
            self._last_mouse_x, self._last_mouse_y = mx, my

History

This bug was introduced in commit 187848f4. That refactor ("replace click-following with idle wander behavior") replaced the prior click-to-follow logic — which early-returned from render() on if abs(mx) > 100_000 or abs(my) > 100_000: return, so the sentinel never reached any state — with the new idle-wander/sprint-back design, and in doing so split the validity check into a non-returning guard that freezes _last_mouse_* while computing mouse_moved before that guard runs.

Dominant language
Python
Stars
4
Forks
0
Avg merge
17h 43m
Merged PRs (30d)
39

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from XKNX/xknxtoolkit

All issues in XKNX/xknxtoolkit

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.