Skip to content

Commit daf80ff

Browse files
k4cper-gclaude
andcommitted
feat: implement full macOS support for actions, screenshots, and app launching
Replace stub implementations with working macOS handlers: - Actions via AXUIElement API + Quartz CGEvents (click, type, scroll, toggle, expand/collapse, select, focus, right-click, double-click, long-press, dismiss) - Keyboard input via CGEvent with Unicode support - App launching with fuzzy name matching via NSWorkspace + open command - Screenshot via screencapture with Screen Recording permission detection - Improved window enumeration combining NSWorkspace + CGWindowListCopyWindowInfo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 324fbe9 commit daf80ff

File tree

4 files changed

+1190
-26
lines changed

4 files changed

+1190
-26
lines changed

cup/__init__.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,15 +416,122 @@ def screenshot(
416416
) -> bytes:
417417
"""Capture a screenshot and return PNG bytes.
418418
419-
Requires the ``mss`` package: ``pip install cup[screenshot]``
419+
On macOS, uses the ``screencapture`` system utility and checks
420+
Screen Recording permission upfront — raises RuntimeError with
421+
a clear message if the permission is missing.
422+
423+
On other platforms, requires the ``mss`` package:
424+
``pip install cup[screenshot]``
420425
421426
Args:
422427
region: Optional capture region {"x", "y", "w", "h"} in pixels.
423428
If None, captures the full primary monitor.
424429
425430
Returns:
426431
PNG image bytes.
432+
433+
Raises:
434+
RuntimeError: On macOS if Screen Recording permission is not
435+
granted (System Settings > Privacy & Security > Screen Recording).
436+
ImportError: On other platforms if ``mss`` is not installed.
437+
"""
438+
import sys
439+
440+
if sys.platform == "darwin":
441+
return self._screenshot_macos(region)
442+
443+
return self._screenshot_mss(region)
444+
445+
def _screenshot_macos(self, region: dict[str, int] | None) -> bytes:
446+
"""macOS screenshot via the ``screencapture`` system utility.
447+
448+
All macOS screenshot APIs (mss, Quartz CGWindowListCreateImage,
449+
and screencapture) return only the desktop wallpaper when the
450+
calling process lacks Screen Recording permission. We detect
451+
this upfront and raise a clear error instead of returning a
452+
useless desktop-only image.
427453
"""
454+
self._check_macos_screen_recording_permission()
455+
456+
import os
457+
import subprocess
458+
import tempfile
459+
460+
fd, tmp_path = tempfile.mkstemp(suffix=".png")
461+
os.close(fd)
462+
463+
try:
464+
cmd = ["screencapture", "-x"] # -x = no sound
465+
466+
if region is not None:
467+
cmd.extend([
468+
"-R",
469+
f"{region['x']},{region['y']},{region['w']},{region['h']}",
470+
])
471+
472+
cmd.append(tmp_path)
473+
474+
result = subprocess.run(cmd, capture_output=True, timeout=10)
475+
if result.returncode != 0:
476+
stderr = result.stderr.decode(errors="replace").strip()
477+
raise RuntimeError(
478+
f"screencapture failed (exit {result.returncode}): {stderr}"
479+
)
480+
481+
with open(tmp_path, "rb") as f:
482+
data = f.read()
483+
484+
if not data:
485+
raise RuntimeError("screencapture produced an empty file")
486+
487+
return data
488+
finally:
489+
try:
490+
os.unlink(tmp_path)
491+
except OSError:
492+
pass
493+
494+
@staticmethod
495+
def _check_macos_screen_recording_permission() -> None:
496+
"""Check if this process has Screen Recording permission.
497+
498+
Without it, all screenshot APIs silently return only the desktop
499+
wallpaper with no application windows visible. We detect this by
500+
checking if CGWindowListCopyWindowInfo returns any window names —
501+
macOS strips them when the process lacks permission.
502+
503+
If permission is missing, we call CGRequestScreenCaptureAccess()
504+
to trigger the system prompt and raise a clear error.
505+
"""
506+
from Quartz import (
507+
CGWindowListCopyWindowInfo,
508+
kCGNullWindowID,
509+
kCGWindowListOptionOnScreenOnly,
510+
)
511+
512+
windows = CGWindowListCopyWindowInfo(
513+
kCGWindowListOptionOnScreenOnly, kCGNullWindowID,
514+
)
515+
516+
# If any window has a name, we have permission
517+
has_permission = any(w.get("kCGWindowName") for w in (windows or []))
518+
519+
if not has_permission:
520+
# Trigger the macOS permission prompt
521+
try:
522+
from Quartz import CGRequestScreenCaptureAccess
523+
CGRequestScreenCaptureAccess()
524+
except ImportError:
525+
pass
526+
527+
raise RuntimeError(
528+
"Screen Recording permission is required for screenshots. "
529+
"Grant it to this app in: System Settings > Privacy & Security "
530+
"> Screen Recording. You may need to restart the app after granting."
531+
)
532+
533+
def _screenshot_mss(self, region: dict[str, int] | None) -> bytes:
534+
"""Fallback screenshot via mss (Windows/Linux)."""
428535
try:
429536
import mss
430537
import mss.tools

0 commit comments

Comments
 (0)