feat(aw): scan both active foreground and visible desktop windows for distractions
Previously, fallback window detection only checked user32.GetForegroundWindow(). If the user had YouTube Shorts open in Chrome on a second monitor or behind the terminal, it was ignored. Now check_for_distraction(): 1. Checks the active foreground window first. 2. If not a distraction, scans all visible top-level desktop windows via find_window_by_match(). 3. Immediately wakes the cat from sleeping or sitting if any distraction window is discovered.
This commit is contained in:
+55
-36
@@ -169,57 +169,76 @@ class ActivityWatchClient:
|
||||
"""Signals the background worker to exit."""
|
||||
self._running = False
|
||||
|
||||
def check_for_distraction(self) -> Optional[DistractionEvent]:
|
||||
"""
|
||||
Checks current active window against distraction rules.
|
||||
Uses ActivityWatch if available, otherwise falls back to native Win32.
|
||||
"""
|
||||
app_name = ""
|
||||
window_title = ""
|
||||
active_window_info: Optional[WindowInfo] = None
|
||||
|
||||
# 1. Try ActivityWatch
|
||||
aw_event = self.get_current_window_event()
|
||||
if aw_event:
|
||||
app_name = (aw_event.get("app") or "").lower()
|
||||
window_title = aw_event.get("title") or ""
|
||||
|
||||
# Locate corresponding Win32 HWND for screen coordinates and close button
|
||||
active_window_info = WindowManager.find_window_by_match(
|
||||
title_predicate=lambda t: window_title.lower() in t.lower() or t.lower() in window_title.lower(),
|
||||
process_predicate=lambda p: app_name in p.lower(),
|
||||
)
|
||||
|
||||
# 2. Fallback to native Win32 if ActivityWatch is unavailable or HWND not found
|
||||
if not active_window_info and self.config.auto_fallback_to_win32:
|
||||
active_window_info = WindowManager.get_foreground_window_info()
|
||||
if active_window_info:
|
||||
app_name = active_window_info.process_name.lower()
|
||||
window_title = active_window_info.title
|
||||
|
||||
if not active_window_info or not window_title:
|
||||
return None
|
||||
|
||||
def _match_event(self, app_name: str, window_title: str, win_info: WindowInfo) -> Optional[DistractionEvent]:
|
||||
"""Evaluates a window title and process name against distraction rules."""
|
||||
title_lower = window_title.lower()
|
||||
app_lower = app_name.lower()
|
||||
|
||||
# Check for title keyword matches (e.g. "shorts", "reels", "tiktok")
|
||||
for kw in self.config.distraction_keywords:
|
||||
if kw.lower() in title_lower:
|
||||
return DistractionEvent(
|
||||
app=app_name,
|
||||
title=window_title,
|
||||
matched_rule=f"keyword '{kw}'",
|
||||
window_info=active_window_info,
|
||||
window_info=win_info,
|
||||
)
|
||||
|
||||
# Check for application process matches
|
||||
for target_app in self.config.distraction_apps:
|
||||
if target_app.lower() in app_name:
|
||||
if target_app.lower() in app_lower:
|
||||
return DistractionEvent(
|
||||
app=app_name,
|
||||
title=window_title,
|
||||
matched_rule=f"app '{target_app}'",
|
||||
window_info=active_window_info,
|
||||
window_info=win_info,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def check_for_distraction(self) -> Optional[DistractionEvent]:
|
||||
"""
|
||||
Checks current active window and visible background windows against distraction rules.
|
||||
Uses ActivityWatch if available, otherwise falls back to native Win32.
|
||||
"""
|
||||
# 1. Try ActivityWatch (non-blocking read of cached event)
|
||||
aw_event = self.get_current_window_event()
|
||||
if aw_event:
|
||||
app_name = (aw_event.get("app") or "").lower()
|
||||
window_title = aw_event.get("title") or ""
|
||||
|
||||
active_window_info = WindowManager.find_window_by_match(
|
||||
title_predicate=lambda t: window_title.lower() in t.lower() or t.lower() in window_title.lower(),
|
||||
process_predicate=lambda p: app_name in p.lower(),
|
||||
)
|
||||
if active_window_info:
|
||||
matched = self._match_event(app_name, window_title, active_window_info)
|
||||
if matched:
|
||||
return matched
|
||||
|
||||
if not self.config.auto_fallback_to_win32:
|
||||
return None
|
||||
|
||||
# 2. Native Win32: Check foreground window first
|
||||
fg_info = WindowManager.get_foreground_window_info()
|
||||
if fg_info and fg_info.title:
|
||||
matched = self._match_event(fg_info.process_name, fg_info.title, fg_info)
|
||||
if matched:
|
||||
return matched
|
||||
|
||||
# 3. Native Win32: If foreground window is not a distraction, check any visible desktop window!
|
||||
# This catches distraction windows open in the background, on a 2nd monitor, or when user clicks away
|
||||
def title_matches(t: str) -> bool:
|
||||
tl = t.lower()
|
||||
return any(kw.lower() in tl for kw in self.config.distraction_keywords)
|
||||
|
||||
def app_matches(p: str) -> bool:
|
||||
pl = p.lower()
|
||||
return any(a.lower() in pl for a in self.config.distraction_apps)
|
||||
|
||||
matched_window = WindowManager.find_window_by_match(
|
||||
title_predicate=title_matches if self.config.distraction_keywords else None,
|
||||
process_predicate=app_matches if self.config.distraction_apps else None,
|
||||
)
|
||||
if matched_window and matched_window.title:
|
||||
return self._match_event(matched_window.process_name, matched_window.title, matched_window)
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user