@@ -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