80bef6f524
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
185 lines
6.2 KiB
Python
185 lines
6.2 KiB
Python
"""What's on screen right now — the active window's title, and whether
|
|
something is running fullscreen.
|
|
|
|
Two consumers:
|
|
* annotate() tacks the focused window's title onto what you said, so
|
|
"what's this error?" has a referent without you describing it (the desk
|
|
API takes text only, so this is a text annotation — no screenshot upload).
|
|
* is_fullscreen_active() feeds do-not-disturb: the pet shouldn't announce
|
|
anything over a call or a fullscreen game.
|
|
|
|
Everything here is best-effort and must never raise: on a locked-down Wayland
|
|
session none of it is available, and the correct behaviour is simply "no
|
|
context", not a crashed pipeline. The subprocess *parsing* is split into pure
|
|
functions so it can be tested without a display server.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
_TIMEOUT = 2.0
|
|
_MAX_TITLE_CHARS = 160
|
|
|
|
# Titles that are just the desktop itself — annotating with these is noise.
|
|
_BORING_TITLES = {"", "desktop", "@!0,0;bdib", "plasmashell", "gnome-shell", "xfdesktop"}
|
|
|
|
|
|
def _run(args: list[str]) -> Optional[str]:
|
|
try:
|
|
completed = subprocess.run(args, capture_output=True, text=True, timeout=_TIMEOUT)
|
|
except Exception:
|
|
return None
|
|
if completed.returncode != 0:
|
|
return None
|
|
return completed.stdout
|
|
|
|
|
|
# ── pure parsing helpers (unit tested; no display server needed) ─────────────
|
|
|
|
def parse_xprop_window_id(output: str) -> Optional[str]:
|
|
"""`xprop -root _NET_ACTIVE_WINDOW` -> '_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00007'"""
|
|
match = re.search(r"(0x[0-9a-fA-F]+)", output or "")
|
|
if not match or int(match.group(1), 16) == 0:
|
|
return None
|
|
return match.group(1)
|
|
|
|
|
|
def parse_xprop_window_name(output: str) -> Optional[str]:
|
|
"""`xprop -id <id> _NET_WM_NAME` -> '_NET_WM_NAME(UTF8_STRING) = "Firefox"'"""
|
|
match = re.search(r'=\s*"(.*)"\s*$', (output or "").strip(), re.DOTALL)
|
|
if not match:
|
|
return None
|
|
return match.group(1).strip()
|
|
|
|
|
|
def parse_xprop_fullscreen(output: str) -> bool:
|
|
return "_NET_WM_STATE_FULLSCREEN" in (output or "")
|
|
|
|
|
|
def clean_title(title: Optional[str]) -> Optional[str]:
|
|
title = (title or "").strip().replace("\n", " ")
|
|
if title.lower() in _BORING_TITLES:
|
|
return None
|
|
if len(title) > _MAX_TITLE_CHARS:
|
|
title = title[: _MAX_TITLE_CHARS - 1].rstrip() + "…"
|
|
return title or None
|
|
|
|
|
|
def annotate(text: str, title: Optional[str]) -> str:
|
|
"""Attach the window title as an explicit aside rather than splicing it
|
|
into the sentence, so the model can ignore it when it's irrelevant."""
|
|
text = (text or "").strip()
|
|
title = clean_title(title)
|
|
if not text or not title:
|
|
return text
|
|
return f"{text}\n\n[on screen right now: {title}]"
|
|
|
|
|
|
# ── platform probes ──────────────────────────────────────────────────────────
|
|
|
|
def _linux_active_window_id() -> Optional[str]:
|
|
if not shutil.which("xprop"):
|
|
return None
|
|
output = _run(["xprop", "-root", "_NET_ACTIVE_WINDOW"])
|
|
return parse_xprop_window_id(output or "")
|
|
|
|
|
|
def _linux_title() -> Optional[str]:
|
|
if shutil.which("xdotool"):
|
|
output = _run(["xdotool", "getactivewindow", "getwindowname"])
|
|
if output and output.strip():
|
|
return output.strip()
|
|
window_id = _linux_active_window_id()
|
|
if window_id is None:
|
|
return None
|
|
for prop in ("_NET_WM_NAME", "WM_NAME"):
|
|
output = _run(["xprop", "-id", window_id, prop])
|
|
title = parse_xprop_window_name(output or "")
|
|
if title:
|
|
return title
|
|
return None
|
|
|
|
|
|
def _windows_title() -> Optional[str]:
|
|
try:
|
|
import ctypes
|
|
|
|
user32 = ctypes.windll.user32
|
|
handle = user32.GetForegroundWindow()
|
|
if not handle:
|
|
return None
|
|
length = user32.GetWindowTextLengthW(handle)
|
|
buffer = ctypes.create_unicode_buffer(length + 1)
|
|
user32.GetWindowTextW(handle, buffer, length + 1)
|
|
return buffer.value or None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _macos_title() -> Optional[str]:
|
|
script = (
|
|
'tell application "System Events" to get name of first application process '
|
|
"whose frontmost is true"
|
|
)
|
|
output = _run(["osascript", "-e", script])
|
|
return (output or "").strip() or None
|
|
|
|
|
|
def active_window_title() -> Optional[str]:
|
|
"""Focused window's title, or None if the platform won't tell us."""
|
|
try:
|
|
system = platform.system()
|
|
if system == "Linux":
|
|
return clean_title(_linux_title())
|
|
if system == "Windows":
|
|
return clean_title(_windows_title())
|
|
if system == "Darwin":
|
|
return clean_title(_macos_title())
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def is_fullscreen_active() -> bool:
|
|
"""True when the focused window is fullscreen (call, game, presentation).
|
|
False whenever we can't tell — do-not-disturb should be something you opt
|
|
into, not something a failed probe turns on."""
|
|
try:
|
|
system = platform.system()
|
|
if system == "Linux":
|
|
window_id = _linux_active_window_id()
|
|
if window_id is None:
|
|
return False
|
|
return parse_xprop_fullscreen(_run(["xprop", "-id", window_id, "_NET_WM_STATE"]) or "")
|
|
if system == "Windows":
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
user32 = ctypes.windll.user32
|
|
handle = user32.GetForegroundWindow()
|
|
if not handle:
|
|
return False
|
|
rect = wintypes.RECT()
|
|
user32.GetWindowRect(handle, ctypes.byref(rect))
|
|
screen_w = user32.GetSystemMetrics(0)
|
|
screen_h = user32.GetSystemMetrics(1)
|
|
return (rect.right - rect.left) >= screen_w and (rect.bottom - rect.top) >= screen_h
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
|
|
def context_for(text: str) -> str:
|
|
"""What the controller sends: *text* plus the active window title, when
|
|
SCREEN_CONTEXT is on and there's a title worth mentioning."""
|
|
from . import config
|
|
|
|
if not config.SCREEN_CONTEXT:
|
|
return text
|
|
return annotate(text, active_window_title())
|