macOS Window Toolkit

pub package License: MIT

A Flutter plugin for macOS window management, screen capture, and application discovery.

Features

  • 🪟 Window Management - Get, search, filter, close, and focus windows
  • 📸 Screen Capture - Capture windows with ScreenCaptureKit or legacy methods
  • 📱 App Discovery - Find and manage installed applications
  • 🔐 Permission Management - Handle screen recording and accessibility permissions
  • 🎯 Process Control - Terminate applications and manage process trees
  • ⚡️ Real-time Monitoring - Monitor permission changes with streams

Installation

dependencies:
  macos_window_toolkit: ^1.8.4

Setup (Required)

1. Disable App Sandbox

Edit macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.automation.apple-events</key>
<true/>

2. Request Permissions at Runtime

final toolkit = MacosWindowToolkit();

// Request screen recording permission (for window names and capture)
await toolkit.requestScreenRecordingPermission();

// Request accessibility permission (for window control and process management)
await toolkit.requestAccessibilityPermission();

Quick Start

import 'package:macos_window_toolkit/macos_window_toolkit.dart';

final toolkit = MacosWindowToolkit();

// Get all windows
final windows = await toolkit.getAllWindows();
for (final window in windows) {
  print('${window.name} - ${window.ownerName}');
}

// Search windows
final chromeWindows = await toolkit.getWindowsByOwnerName('Chrome');

// Close a window
final result = await toolkit.closeWindow(windowId);

// Capture a window
final capture = await toolkit.captureWindow(windowId);
if (capture case CaptureSuccess(imageData: final data)) {
  // Save or display image data
}

// Find installed apps
final apps = await toolkit.getAllInstalledApplications();
if (apps case ApplicationSuccess(applications: final appList)) {
  print('Found ${appList.length} apps');
}

API Reference

Window Management (11 methods)

Method Description
getAllWindows() Get all open windows
getWindowsByName(String name) Search windows by title
getWindowsByOwnerName(String owner) Search by app name
getWindowById(int id) Find window by ID
getWindowsByProcessId(int pid) Find windows by process
getWindowsAdvanced({...}) Advanced filtering (14 parameters)
isWindowAlive(int id) Check if window exists
isWindowFullScreen(int id) Check fullscreen state across Spaces (requires Screen Recording)
closeWindow(int id) Close a window, including windows on other Spaces (requires Accessibility)
focusWindow(int id) Focus/bring window to front (requires Accessibility)
getScrollInfo(int id) Get scroll position (requires Accessibility)

Screen Capture (2 methods)

Method Description
captureWindow(int id, {...}) Capture window using ScreenCaptureKit (macOS 14.0+). Transparent borders are automatically cropped. Supports titlebar exclusion, resize, custom crop, and crop size control.
getCapturableWindows() List capturable windows using ScreenCaptureKit (macOS 12.3+)

Permission Management (10 methods)

Method Description
hasScreenRecordingPermission() Check screen recording permission
requestScreenRecordingPermission() Request screen recording permission
openScreenRecordingSettings() Open system settings
hasAccessibilityPermission() Check accessibility permission
requestAccessibilityPermission() Request accessibility permission
openAccessibilitySettings() Open system settings
startPermissionWatching({interval, emitOnlyChanges}) Start periodic permission monitoring
stopPermissionWatching() Stop permission monitoring
permissionStream Stream of permission status changes
isPermissionWatching Whether monitoring is currently active

Application Management (5 methods)

Method Description
getAllInstalledApplications() Get all installed apps
getApplicationByName(String name) Search apps by name
openAppStoreSearch(String term) Open App Store search
terminateApplicationByPID(int pid) Terminate app (requires Accessibility)
terminateApplicationTree(int pid) Terminate app and children (requires Accessibility)

System Info (4 methods)

Method Description
getMacOSVersionInfo() Get macOS version and capabilities
getChildProcesses(int pid) Get child process IDs
getScreenScaleFactor() Get main screen's Retina scale (1.0, 2.0, etc.)
getAllScreensInfo() Get all screens info (size, pixels, scale)

Permission Requirements

Feature Screen Recording Accessibility
Get window list
Get window names
Window capture
Fullscreen detection
Close/focus windows
Terminate processes
Window role/subrole
Get scroll info

Data Models

MacosWindowInfo

class MacosWindowInfo {
  final int windowId;
  final String name;           // Empty if no screen recording permission
  final String ownerName;
  final double x, y, width, height;
  final int layer;
  final bool isOnScreen;
  final int processId;
  final String? role;          // Requires accessibility permission
  final String? subrole;       // Requires accessibility permission
}

MacosApplicationInfo

class MacosApplicationInfo {
  final String name;
  final String bundleId;
  final String version;
  final String path;
  final String iconPath;
}

MacosScreenInfo

class MacosScreenInfo {
  final int index;
  final bool isMain;
  final double scaleFactor;
  final ScreenRect frame;           // Full screen frame
  final ScreenRect visibleFrame;    // Excluding menu bar/dock
  final int pixelWidth;
  final int pixelHeight;
}

class ScreenRect {
  final double x, y, width, height;
}

Examples

Permission Monitoring with Stream

toolkit.startPermissionWatching(
  interval: Duration(seconds: 2),
  emitOnlyChanges: true,
);

toolkit.permissionStream.listen((status) {
  print('Screen Recording: ${status.screenRecording}');
  print('Accessibility: ${status.accessibility}');
  if (status.allPermissionsGranted) {
    print('All ready!');
  }
});

Advanced Window Filtering

final windows = await toolkit.getWindowsAdvanced(
  ownerName: 'Chrome',
  ownerNameCaseSensitive: false,
  isOnScreen: true,
  width: 800,  // Exact width
);

Get Screen Information

final screens = await toolkit.getAllScreensInfo();
for (final screen in screens) {
  print('Screen ${screen.index}: ${screen.isMain ? "(Main)" : ""}');
  print('  Scale: ${screen.scaleFactor}x');
  print('  Size: ${screen.frame.width} x ${screen.frame.height}');
  print('  Pixels: ${screen.pixelWidth} x ${screen.pixelHeight}');
}

Fullscreen Detection

// Accurate across all Spaces (uses SCShareableContent, not CGWindowList)
final isFullScreen = await toolkit.isWindowFullScreen(windowId);
if (isFullScreen) {
  print('Window is in fullscreen mode');
}

Window Capture with Crop

// Basic capture (transparent borders are auto-cropped)
final capture = await toolkit.captureWindow(windowId);

// Exclude titlebar
final capture = await toolkit.captureWindow(
  windowId,
  excludeTitlebar: true,
  customTitlebarHeight: 44,  // optional, default 28pt
);

// Resize output
final capture = await toolkit.captureWindow(
  windowId,
  targetWidth: 1920,
  targetHeight: 1080,
  preserveAspectRatio: true,  // fills extra space with black
);

// Center crop (crops to given size from the center of the image)
final capture = await toolkit.captureWindow(
  windowId,
  cropContentWidth: 1680,
  cropContentHeight: 945,
);

// Rect crop (crops to an exact rectangle)
final capture = await toolkit.captureWindow(
  windowId,
  cropX: 100,
  cropY: 50,
  cropWidth: 800,
  cropHeight: 600,
);

// Return actual content size after transparent border crop (no stretching)
// Default (true): transparent borders removed, then image stretched back to window frame size
// false: returns the naturally cropped content size as-is
final capture = await toolkit.captureWindow(
  windowId,
  resizeCroppedToWindowSize: false,
);

if (capture case CaptureSuccess(imageData: final data)) {
  // Use image data
}

Get Scroll Position

final result = await toolkit.getScrollInfo(windowId);
switch (result) {
  case ScrollSuccess(scrollInfo: final info):
    print('Vertical: ${info.verticalPosition}');  // 0.0 (top) ~ 1.0 (bottom)
  case ScrollFailure(:final reason, :final message):
    switch (reason) {
      case ScrollFailureReason.windowNotFound:
        print('Window no longer exists');
      case ScrollFailureReason.windowNotAccessible:
        // Window exists but AX API cannot access it
        // Common cause: window is on a different Space
        print('Window not accessible — switch to the Space where it is located');
      case ScrollFailureReason.accessibilityPermissionDenied:
        await toolkit.openAccessibilitySettings();
      case ScrollFailureReason.noScrollableContent:
        print('No scrollable content');
      case ScrollFailureReason.unknown:
        print('Failed: $message');
    }
}

Error Handling

try {
  final windows = await toolkit.getAllWindows();
} on PlatformException catch (e) {
  switch (e.code) {
    case 'SCREEN_RECORDING_PERMISSION_DENIED':
      await toolkit.openScreenRecordingSettings();
    case 'ACCESSIBILITY_PERMISSION_DENIED':
      await toolkit.openAccessibilitySettings();
  }
}

Close Window Error Handling

final result = await toolkit.closeWindow(windowId);
switch (result) {
  case OperationSuccess():
    print('Window closed');
  case OperationFailure(:final code, :final message):
    switch (code) {
      case 'WINDOW_NOT_FOUND':
        print('Window no longer exists');
      case 'WINDOW_NOT_ACCESSIBLE':
        // Window exists but AX API cannot access it
        print('Window not accessible: $message');
      case 'SPACE_SWITCH_API_UNAVAILABLE':
        // Window is on a different Space but the private CGS APIs
        // required to switch Spaces are not available on this macOS version
        print('Cannot switch Space to reach window: $message');
      case 'ACCESSIBILITY_PERMISSION_DENIED':
        await toolkit.openAccessibilitySettings();
    }
}

Requirements

  • macOS 10.15+ (14.0+ required for window capture)
  • Flutter 3.10.0+
  • Dart 3.0.0+
  • App Sandbox must be disabled

App Store Distribution

⚠️ Not recommended for App Store distribution due to sandbox requirements. This plugin requires system-level access that conflicts with App Store sandboxing policies. Consider:

  • Distributing outside the App Store
  • Requesting special entitlements from Apple (rarely approved)

Example App

cd example/
flutter run -d macos

Documentation

License

MIT License - see LICENSE file for details.


Made with ❤️ for the Flutter community