[Detail Bug] Cat plugin: decorative cat stops wandering when app window loses focus (alt-tab/minimize)
Nobody has claimed this yet.
Assessment
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Newbie friendliness
- 90/100
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
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.mdlistscatas 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 commit187848f("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_movedis computed fromio.mouse_posbefore 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_movedisTrueon 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 setsio.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_DISTof 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_posto(-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_movedis computed before the validity guard. With(mx, my) == (-FLT_MAX, -FLT_MAX)and_last_mouse_*frozen,abs(mx - last) > _MOUSE_MOVE_THRESHOLDisTrueevery 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from XKNX/xknxtoolkit
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
XKNX/xknxtoolkit#147 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
XKNX/xknxtoolkit#109 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
XKNX/xknxtoolkit#104 ·
-
[Detail Bug] Dynamic UI evaluation crashes or mangles labels when TextArg values contain backslashes Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
XKNX/xknxtoolkit#100 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
XKNX/xknxtoolkit#98 ·
All issues in XKNX/xknxtoolkit
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100