mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
743 lines
27 KiB
Python
743 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""Windows virtual cursor overlay.
|
|
|
|
The first Windows implementation drew a 168x30 text banner beside the system
|
|
cursor. Apart from looking unlike the macOS feature,
|
|
that window followed the user's pointer as well as Computer Use and was not
|
|
per-monitor-DPI aware, so it could float over the wrong application and at the
|
|
wrong coordinates.
|
|
|
|
This process renders the same visual language as the native macOS helper: a
|
|
blue pointer with a white keyline, breathing halo, idle bob, and click ripple.
|
|
It observes the low-level mouse stream and follows injected movement only. The
|
|
real Windows pointer still has to move because Windows has no ``postToPid``
|
|
equivalent, but the virtual cursor is co-located with that pointer during agent
|
|
movement and stays at the agent's last point if the user takes the mouse.
|
|
|
|
The parent sends one JSON line for each action and closes stdin at turn end.
|
|
The window is per-pixel-alpha, topmost, click-through, absent from Alt-Tab, and
|
|
never activates. Visual failures remain advisory and never block input.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ctypes
|
|
import json
|
|
import math
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from ctypes import wintypes
|
|
|
|
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
|
|
|
|
|
user32 = ctypes.windll.user32
|
|
gdi32 = ctypes.windll.gdi32
|
|
kernel32 = ctypes.windll.kernel32
|
|
|
|
WS_EX_LAYERED = 0x00080000
|
|
WS_EX_TRANSPARENT = 0x00000020
|
|
WS_EX_TOPMOST = 0x00000008
|
|
WS_EX_TOOLWINDOW = 0x00000080
|
|
WS_EX_NOACTIVATE = 0x08000000
|
|
WS_POPUP = 0x80000000
|
|
|
|
SW_HIDE = 0
|
|
SW_SHOWNOACTIVATE = 4
|
|
HWND_TOPMOST = wintypes.HWND(-1)
|
|
SWP_NOACTIVATE = 0x0010
|
|
|
|
WM_DESTROY = 0x0002
|
|
WM_CLOSE = 0x0010
|
|
WM_NCHITTEST = 0x0084
|
|
WM_TIMER = 0x0113
|
|
WM_MOUSEMOVE = 0x0200
|
|
WM_LBUTTONDOWN = 0x0201
|
|
WM_LBUTTONUP = 0x0202
|
|
WM_RBUTTONDOWN = 0x0204
|
|
WM_RBUTTONUP = 0x0205
|
|
WM_MBUTTONDOWN = 0x0207
|
|
WM_MBUTTONUP = 0x0208
|
|
WM_MOUSEWHEEL = 0x020A
|
|
WM_MOUSEHWHEEL = 0x020E
|
|
|
|
HTTRANSPARENT = -1
|
|
WH_MOUSE_LL = 14
|
|
HC_ACTION = 0
|
|
LLMHF_INJECTED = 0x00000001
|
|
LLMHF_LOWER_IL_INJECTED = 0x00000002
|
|
|
|
ULW_ALPHA = 0x00000002
|
|
AC_SRC_OVER = 0x00
|
|
AC_SRC_ALPHA = 0x01
|
|
DIB_RGB_COLORS = 0
|
|
BI_RGB = 0
|
|
GW_HWNDNEXT = 2
|
|
GWL_EXSTYLE = -20
|
|
MONITOR_DEFAULTTONEAREST = 2
|
|
MDT_EFFECTIVE_DPI = 0
|
|
INPUT_TAG_ENV = "CC_HAHA_COMPUTER_USE_INPUT_TAG"
|
|
|
|
# Windows 10 1703+. Negative pseudo-handles are the documented ABI values.
|
|
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ctypes.c_void_p(-4)
|
|
|
|
# Logical-point geometry mirrors VirtualCursorStyle.swift. The overlay is
|
|
# scaled to the current monitor's DPI before it reaches UpdateLayeredWindow.
|
|
CANVAS_SIZE = 76
|
|
HOTSPOT = (32.0, 30.0)
|
|
ARROW_HEIGHT = 22.0
|
|
ARROW_POINTS = (
|
|
(0.0, 0.0),
|
|
(0.0, 16.5),
|
|
(4.0, 12.7),
|
|
(6.6, 18.5),
|
|
(9.2, 17.4),
|
|
(6.6, 11.6),
|
|
(11.6, 11.2),
|
|
)
|
|
HALO_DIAMETER = ARROW_HEIGHT * 2.4
|
|
IDLE_BOB_AMPLITUDE = 1.5
|
|
IDLE_BOB_PERIOD = 1.8
|
|
HALO_BREATH_PERIOD = 2.4
|
|
TIMER_ID = 1
|
|
TIMER_INTERVAL_MS = 16
|
|
|
|
|
|
class POINT(ctypes.Structure):
|
|
_fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
|
|
|
|
|
|
class SIZE(ctypes.Structure):
|
|
_fields_ = [("cx", wintypes.LONG), ("cy", wintypes.LONG)]
|
|
|
|
|
|
class RECT(ctypes.Structure):
|
|
_fields_ = [
|
|
("left", wintypes.LONG),
|
|
("top", wintypes.LONG),
|
|
("right", wintypes.LONG),
|
|
("bottom", wintypes.LONG),
|
|
]
|
|
|
|
|
|
class BLENDFUNCTION(ctypes.Structure):
|
|
_fields_ = [
|
|
("BlendOp", ctypes.c_ubyte),
|
|
("BlendFlags", ctypes.c_ubyte),
|
|
("SourceConstantAlpha", ctypes.c_ubyte),
|
|
("AlphaFormat", ctypes.c_ubyte),
|
|
]
|
|
|
|
|
|
class BITMAPINFOHEADER(ctypes.Structure):
|
|
_fields_ = [
|
|
("biSize", wintypes.DWORD),
|
|
("biWidth", wintypes.LONG),
|
|
("biHeight", wintypes.LONG),
|
|
("biPlanes", wintypes.WORD),
|
|
("biBitCount", wintypes.WORD),
|
|
("biCompression", wintypes.DWORD),
|
|
("biSizeImage", wintypes.DWORD),
|
|
("biXPelsPerMeter", wintypes.LONG),
|
|
("biYPelsPerMeter", wintypes.LONG),
|
|
("biClrUsed", wintypes.DWORD),
|
|
("biClrImportant", wintypes.DWORD),
|
|
]
|
|
|
|
|
|
class RGBQUAD(ctypes.Structure):
|
|
_fields_ = [
|
|
("rgbBlue", ctypes.c_ubyte),
|
|
("rgbGreen", ctypes.c_ubyte),
|
|
("rgbRed", ctypes.c_ubyte),
|
|
("rgbReserved", ctypes.c_ubyte),
|
|
]
|
|
|
|
|
|
class BITMAPINFO(ctypes.Structure):
|
|
_fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", RGBQUAD * 1)]
|
|
|
|
|
|
class MSLLHOOKSTRUCT(ctypes.Structure):
|
|
_fields_ = [
|
|
("pt", POINT),
|
|
("mouseData", wintypes.DWORD),
|
|
("flags", wintypes.DWORD),
|
|
("time", wintypes.DWORD),
|
|
("dwExtraInfo", ctypes.c_size_t),
|
|
]
|
|
|
|
|
|
LRESULT = ctypes.c_ssize_t
|
|
WNDPROC = ctypes.WINFUNCTYPE(
|
|
LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
|
|
)
|
|
HOOKPROC = ctypes.WINFUNCTYPE(
|
|
LRESULT, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM
|
|
)
|
|
|
|
|
|
class WNDCLASS(ctypes.Structure):
|
|
_fields_ = [
|
|
("style", wintypes.UINT),
|
|
("lpfnWndProc", WNDPROC),
|
|
("cbClsExtra", ctypes.c_int),
|
|
("cbWndExtra", ctypes.c_int),
|
|
("hInstance", wintypes.HINSTANCE),
|
|
("hIcon", wintypes.HICON),
|
|
("hCursor", wintypes.HANDLE),
|
|
("hbrBackground", wintypes.HBRUSH),
|
|
("lpszMenuName", wintypes.LPCWSTR),
|
|
("lpszClassName", wintypes.LPCWSTR),
|
|
]
|
|
|
|
|
|
def _enable_per_monitor_dpi_awareness() -> None:
|
|
"""Make pointer, window, and screenshot coordinates all physical pixels."""
|
|
try:
|
|
setter = user32.SetProcessDpiAwarenessContext
|
|
setter.argtypes = [wintypes.HANDLE]
|
|
setter.restype = wintypes.BOOL
|
|
if setter(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2):
|
|
return
|
|
except (AttributeError, OSError):
|
|
pass
|
|
try:
|
|
shcore = ctypes.windll.shcore
|
|
shcore.SetProcessDpiAwareness.argtypes = [ctypes.c_int]
|
|
shcore.SetProcessDpiAwareness.restype = ctypes.c_long
|
|
if shcore.SetProcessDpiAwareness(2) in (0, 0x80070005):
|
|
return
|
|
except (AttributeError, OSError):
|
|
pass
|
|
try:
|
|
user32.SetProcessDPIAware()
|
|
except (AttributeError, OSError):
|
|
pass
|
|
|
|
|
|
def _configure_win32() -> None:
|
|
kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
|
|
kernel32.GetModuleHandleW.restype = wintypes.HINSTANCE
|
|
|
|
user32.RegisterClassW.argtypes = [ctypes.POINTER(WNDCLASS)]
|
|
user32.RegisterClassW.restype = wintypes.WORD
|
|
user32.CreateWindowExW.argtypes = [
|
|
wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD,
|
|
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
|
wintypes.HWND, wintypes.HANDLE, wintypes.HINSTANCE, wintypes.LPVOID,
|
|
]
|
|
user32.CreateWindowExW.restype = wintypes.HWND
|
|
user32.DefWindowProcW.argtypes = [
|
|
wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
|
|
]
|
|
user32.DefWindowProcW.restype = LRESULT
|
|
user32.DestroyWindow.argtypes = [wintypes.HWND]
|
|
user32.DestroyWindow.restype = wintypes.BOOL
|
|
user32.PostQuitMessage.argtypes = [ctypes.c_int]
|
|
user32.PostQuitMessage.restype = None
|
|
user32.PostMessageW.argtypes = [
|
|
wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
|
|
]
|
|
user32.PostMessageW.restype = wintypes.BOOL
|
|
user32.GetMessageW.argtypes = [
|
|
ctypes.POINTER(wintypes.MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT,
|
|
]
|
|
user32.GetMessageW.restype = ctypes.c_int
|
|
user32.TranslateMessage.argtypes = [ctypes.POINTER(wintypes.MSG)]
|
|
user32.TranslateMessage.restype = wintypes.BOOL
|
|
user32.DispatchMessageW.argtypes = [ctypes.POINTER(wintypes.MSG)]
|
|
user32.DispatchMessageW.restype = LRESULT
|
|
user32.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
|
|
user32.ShowWindow.restype = wintypes.BOOL
|
|
user32.SetWindowPos.argtypes = [
|
|
wintypes.HWND, wintypes.HWND, ctypes.c_int, ctypes.c_int,
|
|
ctypes.c_int, ctypes.c_int, wintypes.UINT,
|
|
]
|
|
user32.SetWindowPos.restype = wintypes.BOOL
|
|
user32.SetTimer.argtypes = [wintypes.HWND, ctypes.c_size_t, wintypes.UINT, wintypes.LPVOID]
|
|
user32.SetTimer.restype = ctypes.c_size_t
|
|
user32.KillTimer.argtypes = [wintypes.HWND, ctypes.c_size_t]
|
|
user32.KillTimer.restype = wintypes.BOOL
|
|
|
|
user32.SetWindowsHookExW.argtypes = [
|
|
ctypes.c_int, HOOKPROC, wintypes.HINSTANCE, wintypes.DWORD,
|
|
]
|
|
user32.SetWindowsHookExW.restype = wintypes.HANDLE
|
|
user32.CallNextHookEx.argtypes = [
|
|
wintypes.HANDLE, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM,
|
|
]
|
|
user32.CallNextHookEx.restype = LRESULT
|
|
user32.UnhookWindowsHookEx.argtypes = [wintypes.HANDLE]
|
|
user32.UnhookWindowsHookEx.restype = wintypes.BOOL
|
|
|
|
user32.UpdateLayeredWindow.argtypes = [
|
|
wintypes.HWND, wintypes.HDC, ctypes.POINTER(POINT), ctypes.POINTER(SIZE),
|
|
wintypes.HDC, ctypes.POINTER(POINT), wintypes.DWORD,
|
|
ctypes.POINTER(BLENDFUNCTION), wintypes.DWORD,
|
|
]
|
|
user32.UpdateLayeredWindow.restype = wintypes.BOOL
|
|
user32.GetDpiForWindow.argtypes = [wintypes.HWND]
|
|
user32.GetDpiForWindow.restype = wintypes.UINT
|
|
user32.MonitorFromPoint.argtypes = [POINT, wintypes.DWORD]
|
|
user32.MonitorFromPoint.restype = wintypes.HANDLE
|
|
user32.WindowFromPoint.argtypes = [POINT]
|
|
user32.WindowFromPoint.restype = wintypes.HWND
|
|
user32.GetForegroundWindow.argtypes = []
|
|
user32.GetForegroundWindow.restype = wintypes.HWND
|
|
user32.GetWindowThreadProcessId.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)]
|
|
user32.GetWindowThreadProcessId.restype = wintypes.DWORD
|
|
user32.GetTopWindow.argtypes = [wintypes.HWND]
|
|
user32.GetTopWindow.restype = wintypes.HWND
|
|
user32.GetWindow.argtypes = [wintypes.HWND, wintypes.UINT]
|
|
user32.GetWindow.restype = wintypes.HWND
|
|
user32.IsWindowVisible.argtypes = [wintypes.HWND]
|
|
user32.IsWindowVisible.restype = wintypes.BOOL
|
|
user32.IsIconic.argtypes = [wintypes.HWND]
|
|
user32.IsIconic.restype = wintypes.BOOL
|
|
user32.GetWindowRect.argtypes = [wintypes.HWND, ctypes.POINTER(RECT)]
|
|
user32.GetWindowRect.restype = wintypes.BOOL
|
|
user32.GetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int]
|
|
user32.GetWindowLongW.restype = ctypes.c_long
|
|
|
|
gdi32.CreateCompatibleDC.argtypes = [wintypes.HDC]
|
|
gdi32.CreateCompatibleDC.restype = wintypes.HDC
|
|
gdi32.DeleteDC.argtypes = [wintypes.HDC]
|
|
gdi32.DeleteDC.restype = wintypes.BOOL
|
|
gdi32.CreateDIBSection.argtypes = [
|
|
wintypes.HDC, ctypes.POINTER(BITMAPINFO), wintypes.UINT,
|
|
ctypes.POINTER(ctypes.c_void_p), wintypes.HANDLE, wintypes.DWORD,
|
|
]
|
|
gdi32.CreateDIBSection.restype = wintypes.HBITMAP
|
|
gdi32.SelectObject.argtypes = [wintypes.HDC, wintypes.HANDLE]
|
|
gdi32.SelectObject.restype = wintypes.HANDLE
|
|
gdi32.DeleteObject.argtypes = [wintypes.HANDLE]
|
|
gdi32.DeleteObject.restype = wintypes.BOOL
|
|
|
|
try:
|
|
shcore = ctypes.windll.shcore
|
|
shcore.GetDpiForMonitor.argtypes = [
|
|
wintypes.HANDLE, ctypes.c_int,
|
|
ctypes.POINTER(wintypes.UINT), ctypes.POINTER(wintypes.UINT),
|
|
]
|
|
shcore.GetDpiForMonitor.restype = ctypes.c_long
|
|
except (AttributeError, OSError):
|
|
pass
|
|
|
|
|
|
_enable_per_monitor_dpi_awareness()
|
|
_configure_win32()
|
|
|
|
|
|
def _pid_for_window(hwnd: int | None) -> int | None:
|
|
if not hwnd:
|
|
return None
|
|
pid = wintypes.DWORD()
|
|
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
return int(pid.value) or None
|
|
|
|
|
|
def _input_tag() -> int | None:
|
|
try:
|
|
value = int(os.environ[INPUT_TAG_ENV]) & 0xFFFFFFFF
|
|
return value or None
|
|
except (KeyError, TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _dpi_at_point(point: tuple[int, int], hwnd: int) -> int:
|
|
"""Return the DPI of the destination monitor before moving the overlay."""
|
|
try:
|
|
monitor = user32.MonitorFromPoint(
|
|
POINT(point[0], point[1]), MONITOR_DEFAULTTONEAREST,
|
|
)
|
|
dpi_x = wintypes.UINT()
|
|
dpi_y = wintypes.UINT()
|
|
if monitor and ctypes.windll.shcore.GetDpiForMonitor(
|
|
monitor, MDT_EFFECTIVE_DPI, ctypes.byref(dpi_x), ctypes.byref(dpi_y),
|
|
) == 0:
|
|
return int(dpi_x.value) or 96
|
|
except (AttributeError, OSError):
|
|
pass
|
|
return int(user32.GetDpiForWindow(hwnd)) or 96
|
|
|
|
|
|
def _point_from_activity(command: str, payload: dict) -> tuple[int, int] | None:
|
|
if command == "drag":
|
|
point = payload.get("to") or {}
|
|
if "x" in point and "y" in point:
|
|
return int(point["x"]), int(point["y"])
|
|
return None
|
|
if command in {"click", "move_mouse", "scroll"}:
|
|
if "x" in payload and "y" in payload:
|
|
return int(payload["x"]), int(payload["y"])
|
|
return None
|
|
|
|
|
|
def _premultiplied_bgra(image: Image.Image) -> bytes:
|
|
red, green, blue, alpha = image.split()
|
|
return Image.merge("RGBA", (
|
|
ImageChops.multiply(blue, alpha),
|
|
ImageChops.multiply(green, alpha),
|
|
ImageChops.multiply(red, alpha),
|
|
alpha,
|
|
)).tobytes()
|
|
|
|
|
|
def _render_cursor(scale: float, now: float, click: tuple[float, int] | None) -> Image.Image:
|
|
"""Render one anti-aliased frame using the native helper's dimensions."""
|
|
size = max(1, round(CANVAS_SIZE * scale))
|
|
antialias = 3
|
|
factor = scale * antialias
|
|
image = Image.new("RGBA", (size * antialias, size * antialias), (0, 0, 0, 0))
|
|
|
|
breath = (math.sin((now / HALO_BREATH_PERIOD) * math.tau - math.pi / 2) + 1) / 2
|
|
halo_scale = 0.86 + breath * 0.28
|
|
halo_alpha = 0.5 + breath * 0.5
|
|
arrow_scale = ARROW_HEIGHT / 19.0
|
|
bob = math.sin((now / IDLE_BOB_PERIOD) * math.tau) * IDLE_BOB_AMPLITUDE
|
|
|
|
tip_x = HOTSPOT[0] * factor
|
|
tip_y = HOTSPOT[1] * factor
|
|
arrow_points = [
|
|
(
|
|
tip_x + x * arrow_scale * factor,
|
|
tip_y + (y * arrow_scale + bob) * factor,
|
|
)
|
|
for x, y in ARROW_POINTS
|
|
]
|
|
|
|
arrow_width = max(x for x, _ in ARROW_POINTS) * arrow_scale
|
|
halo_center = (
|
|
tip_x + arrow_width * factor / 2,
|
|
tip_y + ARROW_HEIGHT * factor / 2,
|
|
)
|
|
halo_radius = HALO_DIAMETER * halo_scale * factor / 2
|
|
halo = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
|
halo_draw = ImageDraw.Draw(halo)
|
|
halo_draw.ellipse(
|
|
(
|
|
halo_center[0] - halo_radius,
|
|
halo_center[1] - halo_radius,
|
|
halo_center[0] + halo_radius,
|
|
halo_center[1] + halo_radius,
|
|
),
|
|
fill=(38, 140, 255, round(107 * halo_alpha)),
|
|
)
|
|
halo = halo.filter(ImageFilter.GaussianBlur(max(1, round(7 * factor))))
|
|
image = Image.alpha_composite(image, halo)
|
|
|
|
if click is not None:
|
|
started, kind = click
|
|
progress = min(1.0, max(0.0, (now - started) / 0.38))
|
|
if progress < 1:
|
|
ripple = ImageDraw.Draw(image)
|
|
radius = (7 + 21 * progress) * factor
|
|
alpha = round(230 * (1 - progress))
|
|
color = (255, 149, 0, alpha) if kind == WM_RBUTTONDOWN else (77, 158, 255, alpha)
|
|
width = max(1, round(2.5 * factor))
|
|
ripple.ellipse(
|
|
(tip_x - radius, tip_y - radius, tip_x + radius, tip_y + radius),
|
|
outline=color,
|
|
width=width,
|
|
)
|
|
pulse = 1.0 - 0.2 * math.sin(progress * math.pi)
|
|
arrow_points = [
|
|
(tip_x + (x - tip_x) * pulse, tip_y + (y - tip_y) * pulse)
|
|
for x, y in arrow_points
|
|
]
|
|
|
|
shadow = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
|
shadow_draw = ImageDraw.Draw(shadow)
|
|
shadow_draw.polygon(
|
|
[(x, y + factor) for x, y in arrow_points],
|
|
fill=(0, 0, 0, 90),
|
|
)
|
|
shadow = shadow.filter(ImageFilter.GaussianBlur(max(1, round(3 * factor))))
|
|
image = Image.alpha_composite(image, shadow)
|
|
|
|
arrow = ImageDraw.Draw(image)
|
|
outline_width = max(1, round(1.5 * factor))
|
|
arrow.polygon(arrow_points, fill="#0075f2")
|
|
arrow.line(
|
|
[*arrow_points, arrow_points[0]],
|
|
fill="white",
|
|
width=outline_width,
|
|
joint="curve",
|
|
)
|
|
|
|
return image.resize((size, size), Image.Resampling.LANCZOS)
|
|
|
|
|
|
class VirtualCursorOverlay:
|
|
def __init__(self) -> None:
|
|
self.hwnd: int | None = None
|
|
self._stop = threading.Event()
|
|
self._lock = threading.Lock()
|
|
self._agent_position: tuple[int, int] | None = None
|
|
self._destination: tuple[int, int] | None = None
|
|
self._target_pid: int | None = None
|
|
self._requested_visible = False
|
|
self._hidden_for_user = False
|
|
self._click: tuple[float, int] | None = None
|
|
self._shown = False
|
|
self._mouse_hook: int | None = None
|
|
self._input_tag = _input_tag()
|
|
self._wndproc = WNDPROC(self._on_message)
|
|
self._hookproc = HOOKPROC(self._on_mouse_hook)
|
|
|
|
def _on_message(self, hwnd, msg, wparam, lparam):
|
|
if msg == WM_NCHITTEST:
|
|
return HTTRANSPARENT
|
|
if msg == WM_TIMER and int(wparam) == TIMER_ID:
|
|
self._tick()
|
|
return 0
|
|
if msg == WM_CLOSE:
|
|
user32.DestroyWindow(hwnd)
|
|
return 0
|
|
if msg == WM_DESTROY:
|
|
user32.PostQuitMessage(0)
|
|
return 0
|
|
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
|
|
|
def _on_mouse_hook(self, code, wparam, lparam):
|
|
if code == HC_ACTION:
|
|
info = ctypes.cast(lparam, ctypes.POINTER(MSLLHOOKSTRUCT)).contents
|
|
injected = bool(info.flags & (LLMHF_INJECTED | LLMHF_LOWER_IL_INJECTED))
|
|
agent_injected = (
|
|
injected
|
|
and self._input_tag is not None
|
|
and int(info.dwExtraInfo) & 0xFFFFFFFF == self._input_tag
|
|
)
|
|
event = int(wparam)
|
|
with self._lock:
|
|
if agent_injected:
|
|
self._agent_position = (int(info.pt.x), int(info.pt.y))
|
|
if self._target_pid is None:
|
|
self._target_pid = self._pid_at_point(self._agent_position)
|
|
self._requested_visible = self._target_pid is not None
|
|
self._hidden_for_user = False
|
|
if event in {WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN}:
|
|
self._click = (time.monotonic(), event)
|
|
elif event in {
|
|
WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_LBUTTONUP,
|
|
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_MBUTTONDOWN,
|
|
WM_MBUTTONUP, WM_MOUSEWHEEL, WM_MOUSEHWHEEL,
|
|
}:
|
|
# Physical input means the user has taken the shared Windows
|
|
# pointer. Keep our logical position but hide until the next
|
|
# injected event, so the overlay never follows their hand.
|
|
self._hidden_for_user = True
|
|
return user32.CallNextHookEx(self._mouse_hook, code, wparam, lparam)
|
|
|
|
def _pid_at_point(self, point: tuple[int, int]) -> int | None:
|
|
# This is called before the first reveal for each destination, so the
|
|
# direct API resolves the controlled window rather than this overlay.
|
|
hwnd = user32.WindowFromPoint(POINT(*point))
|
|
if hwnd and int(hwnd) != int(self.hwnd or 0):
|
|
return _pid_for_window(hwnd)
|
|
return self._top_window_pid_at(point)
|
|
|
|
def _top_window_pid_at(self, point: tuple[int, int]) -> int | None:
|
|
hwnd = user32.GetTopWindow(None)
|
|
while hwnd:
|
|
if int(hwnd) != int(self.hwnd or 0) and user32.IsWindowVisible(hwnd) and not user32.IsIconic(hwnd):
|
|
style = int(user32.GetWindowLongW(hwnd, GWL_EXSTYLE))
|
|
rect = RECT()
|
|
if not (style & WS_EX_TRANSPARENT) and user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
|
if rect.left <= point[0] < rect.right and rect.top <= point[1] < rect.bottom:
|
|
pid = _pid_for_window(hwnd)
|
|
if pid and pid != os.getpid():
|
|
return pid
|
|
hwnd = user32.GetWindow(hwnd, GW_HWNDNEXT)
|
|
return None
|
|
|
|
def _target_is_visible(self, target_pid: int, destination: tuple[int, int]) -> bool:
|
|
foreground_pid = _pid_for_window(user32.GetForegroundWindow())
|
|
return foreground_pid == target_pid or self._top_window_pid_at(destination) == target_pid
|
|
|
|
def _on_activity(self, message: dict) -> None:
|
|
command = str(message.get("command") or "")
|
|
payload = message.get("payload")
|
|
if not isinstance(payload, dict):
|
|
payload = {}
|
|
point = _point_from_activity(command, payload)
|
|
raw_target_pid = message.get("targetPid")
|
|
target_pid = int(raw_target_pid) if isinstance(raw_target_pid, int) and raw_target_pid > 0 else None
|
|
|
|
with self._lock:
|
|
if point is not None:
|
|
# Keep the cursor visible between related actions. Hiding here
|
|
# made every movement reappear only after the first injected
|
|
# frame, which looked like a jump into the middle of the path.
|
|
# `_pid_at_point` already skips this transparent overlay.
|
|
self._destination = point
|
|
self._target_pid = target_pid or self._pid_at_point(point)
|
|
if self._agent_position is None:
|
|
current = POINT()
|
|
if user32.GetCursorPos(ctypes.byref(current)):
|
|
self._agent_position = (int(current.x), int(current.y))
|
|
self._requested_visible = self._target_pid is not None
|
|
self._hidden_for_user = False
|
|
elif target_pid is not None:
|
|
self._target_pid = target_pid
|
|
|
|
def _read_parent(self) -> None:
|
|
try:
|
|
for line in sys.stdin:
|
|
try:
|
|
message = json.loads(line)
|
|
if isinstance(message, dict):
|
|
self._on_activity(message)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
except Exception:
|
|
pass
|
|
self.stop()
|
|
|
|
def create(self) -> None:
|
|
hinst = kernel32.GetModuleHandleW(None)
|
|
class_name = "CcHahaVirtualCursorOverlay"
|
|
wc = WNDCLASS()
|
|
wc.lpfnWndProc = self._wndproc
|
|
wc.hInstance = hinst
|
|
wc.lpszClassName = class_name
|
|
wc.hbrBackground = 0
|
|
wc.hCursor = 0
|
|
user32.RegisterClassW(ctypes.byref(wc))
|
|
|
|
self.hwnd = user32.CreateWindowExW(
|
|
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST
|
|
| WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
|
class_name, None, WS_POPUP,
|
|
0, 0, CANVAS_SIZE, CANVAS_SIZE,
|
|
None, None, hinst, None,
|
|
)
|
|
if not self.hwnd:
|
|
raise OSError("CreateWindowExW failed for the virtual cursor")
|
|
|
|
self._mouse_hook = user32.SetWindowsHookExW(WH_MOUSE_LL, self._hookproc, None, 0)
|
|
if not self._mouse_hook:
|
|
raise OSError("SetWindowsHookExW failed for the virtual cursor")
|
|
if not user32.SetTimer(self.hwnd, TIMER_ID, TIMER_INTERVAL_MS, None):
|
|
raise OSError("SetTimer failed for the virtual cursor")
|
|
|
|
def _tick(self) -> None:
|
|
if not self.hwnd:
|
|
return
|
|
with self._lock:
|
|
position = self._agent_position
|
|
destination = self._destination
|
|
target_pid = self._target_pid
|
|
visible = self._requested_visible and not self._hidden_for_user
|
|
click = self._click
|
|
|
|
if visible and position and destination and target_pid:
|
|
visible = self._target_is_visible(target_pid, destination)
|
|
else:
|
|
visible = False
|
|
|
|
if not visible:
|
|
if self._shown:
|
|
user32.ShowWindow(self.hwnd, SW_HIDE)
|
|
self._shown = False
|
|
return
|
|
|
|
assert position is not None
|
|
dpi = _dpi_at_point(position, self.hwnd)
|
|
scale = max(1.0, dpi / 96.0)
|
|
image = _render_cursor(scale, time.monotonic(), click)
|
|
left = round(position[0] - HOTSPOT[0] * scale)
|
|
top = round(position[1] - HOTSPOT[1] * scale)
|
|
self._update_layered_image(image, left, top)
|
|
if not self._shown:
|
|
user32.ShowWindow(self.hwnd, SW_SHOWNOACTIVATE)
|
|
self._shown = True
|
|
|
|
def _update_layered_image(self, image: Image.Image, left: int, top: int) -> None:
|
|
assert self.hwnd is not None
|
|
width, height = image.size
|
|
bmi = BITMAPINFO()
|
|
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
|
bmi.bmiHeader.biWidth = width
|
|
bmi.bmiHeader.biHeight = -height # top-down DIB, same orientation as Pillow
|
|
bmi.bmiHeader.biPlanes = 1
|
|
bmi.bmiHeader.biBitCount = 32
|
|
bmi.bmiHeader.biCompression = BI_RGB
|
|
|
|
bits = ctypes.c_void_p()
|
|
memory_dc = gdi32.CreateCompatibleDC(None)
|
|
bitmap = gdi32.CreateDIBSection(
|
|
memory_dc, ctypes.byref(bmi), DIB_RGB_COLORS,
|
|
ctypes.byref(bits), None, 0,
|
|
)
|
|
if not memory_dc or not bitmap or not bits.value:
|
|
if bitmap:
|
|
gdi32.DeleteObject(bitmap)
|
|
if memory_dc:
|
|
gdi32.DeleteDC(memory_dc)
|
|
return
|
|
|
|
previous = gdi32.SelectObject(memory_dc, bitmap)
|
|
try:
|
|
pixels = _premultiplied_bgra(image)
|
|
ctypes.memmove(bits, pixels, len(pixels))
|
|
destination = POINT(left, top)
|
|
size = SIZE(width, height)
|
|
source = POINT(0, 0)
|
|
blend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA)
|
|
user32.UpdateLayeredWindow(
|
|
self.hwnd, None, ctypes.byref(destination), ctypes.byref(size),
|
|
memory_dc, ctypes.byref(source), 0, ctypes.byref(blend), ULW_ALPHA,
|
|
)
|
|
user32.SetWindowPos(
|
|
self.hwnd, HWND_TOPMOST, left, top, width, height, SWP_NOACTIVATE,
|
|
)
|
|
finally:
|
|
if previous:
|
|
gdi32.SelectObject(memory_dc, previous)
|
|
gdi32.DeleteObject(bitmap)
|
|
gdi32.DeleteDC(memory_dc)
|
|
|
|
def stop(self) -> None:
|
|
self._stop.set()
|
|
if self.hwnd:
|
|
user32.PostMessageW(self.hwnd, WM_CLOSE, 0, 0)
|
|
|
|
def run(self) -> int:
|
|
self.create()
|
|
threading.Thread(target=self._read_parent, daemon=True).start()
|
|
# The TypeScript bridge waits for this handshake before dispatching the
|
|
# first SendInput action, so the low-level hook cannot miss its movement.
|
|
print("READY", flush=True)
|
|
|
|
msg = wintypes.MSG()
|
|
while not self._stop.is_set() and user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
|
|
user32.TranslateMessage(ctypes.byref(msg))
|
|
user32.DispatchMessageW(ctypes.byref(msg))
|
|
|
|
if self.hwnd:
|
|
user32.KillTimer(self.hwnd, TIMER_ID)
|
|
if self._mouse_hook:
|
|
user32.UnhookWindowsHookEx(self._mouse_hook)
|
|
self._mouse_hook = None
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
# Kept for compatibility with already-running parents from the previous
|
|
# runtime; labels are intentionally no longer rendered.
|
|
parser.add_argument("--label", default=None, help=argparse.SUPPRESS)
|
|
parser.parse_args()
|
|
if sys.platform != "win32":
|
|
print("win_cursor_badge.py is Windows-only", file=sys.stderr)
|
|
return 1
|
|
return VirtualCursorOverlay().run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|