"""Thumbnail of a bot's screen: one JPEG grab of the profile's Xvnc display. Feeds the Screen hero in Hermes Desktop (the big preview at the top of a bot's pane). Read-only: it never touches the lease, so a human in control is not disturbed and the bot is not blocked. """ from __future__ import annotations import base64 import io import os import threading from typing import Optional from tools.bot_desktop import runtime THUMB_MAX = (960, 600) _grab_lock = threading.Lock() def thumbnail_data_url(max_size: tuple[int, int] = THUMB_MAX, quality: int = 72) -> Optional[str]: """``data:image/jpeg;base64,...`` of the running screen, or ``None`` when no screen is up.""" env = runtime.published_env() display = env.get("DISPLAY") if not display: return None if runtime.sandbox_screen_running(): from tools.bot_desktop import sandbox_host sandbox = runtime._owned_sandbox_env() jpeg = sandbox_host.grab_jpeg(sandbox, env, max_size, quality) if sandbox is not None else None return "data:image/jpeg;base64," + base64.b64encode(jpeg).decode("ascii") if jpeg else None if runtime._launcher_pid() is None: return None from PIL import ImageGrab # Pillow is a hard dependency; import lazily to keep status calls cheap # Xlib reads XAUTHORITY from the process env; the launcher publishes a per-profile cookie file. # The swap is process-wide, so two profiles grabbed on worker threads at once serialise here or # one would grab with the other's cookie and restore the wrong value. with _grab_lock: previous = os.environ.get("XAUTHORITY") if env.get("XAUTHORITY"): os.environ["XAUTHORITY"] = env["XAUTHORITY"] try: image = ImageGrab.grab(xdisplay=display) finally: if previous is None: os.environ.pop("XAUTHORITY", None) else: os.environ["XAUTHORITY"] = previous image.thumbnail(max_size) buf = io.BytesIO() image.convert("RGB").save(buf, "JPEG", quality=quality, optimize=True) return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")