├── .gitignore ├── install.js ├── binding.gyp ├── example.js ├── index.test-d.ts ├── .github └── workflows │ └── ci.yml ├── package.json ├── index.d.ts ├── index.js ├── screen-capture-permissions.m └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | /.build 4 | /*.xcodeproj 5 | xcuserdata 6 | project.xcworkspace 7 | /screen-capture-permissions 8 | /build 9 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const {spawnSync} = require('child_process'); 3 | 4 | if (os.platform() === 'darwin') { 5 | spawnSync('npm', ['run', 'native_build'], { 6 | input: 'darwin detected. Build native module.', 7 | stdio: 'inherit' 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "screencapturepermissions", 5 | "sources": [ 6 | "screen-capture-permissions.m" 7 | ], 8 | "xcode_settings": { 9 | "MACOSX_DEPLOYMENT_TARGET": "10.14", 10 | "OTHER_LDFLAGS": ["-framework CoreGraphics"] 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const {app} = require('electron'); 2 | const { 3 | hasScreenCapturePermission, 4 | hasPromptedForPermission, 5 | openSystemPreferences 6 | } = require('.'); 7 | 8 | (async () => { 9 | await app.whenReady(); 10 | 11 | console.log('Has asked permissions?', hasPromptedForPermission()); 12 | 13 | console.log('Has permissions?', hasScreenCapturePermission()); 14 | console.log('Has asked permissions?', hasPromptedForPermission()); 15 | 16 | openSystemPreferences(); 17 | })(); 18 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectError} from 'tsd'; 2 | import {hasScreenCapturePermission, hasPromptedForPermission, openSystemPreferences, resetPermissions} from './index.d'; 3 | 4 | expectType(hasScreenCapturePermission()); 5 | expectType(hasPromptedForPermission()); 6 | expectType(resetPermissions()); 7 | expectType(resetPermissions({bundleId: 'some.bundle.id'})); 8 | expectType>(openSystemPreferences()); 9 | 10 | expectError(resetPermissions('some.bundle.id')); 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | build: 11 | name: Build on ${{ matrix.os }} 12 | runs-on: macos-10.15 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: "16" 18 | - run: npm install 19 | - run: npm run test 20 | - name: Release prebuilt artifacts 21 | if: github.event_name == 'release' && github.event.action == 'published' 22 | run: npx prebuild -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -r electron -u ${{ secrets.PUBLISH_TOKEN }} 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mac-screen-capture-permissions", 3 | "version": "2.1.0", 4 | "description": "Check and request permission to capture the screen", 5 | "main": "index.js", 6 | "repository": "https://github.com/karaggeorge/mac-screen-capture-permissions", 7 | "author": { 8 | "name": "George Karagkiaouris", 9 | "email": "gkaragkiaouris2@gmail.com", 10 | "url": "https://gkaragkiaouris.tech" 11 | }, 12 | "license": "MIT", 13 | "private": false, 14 | "scripts": { 15 | "start": "electron example.js", 16 | "test": "xo && tsd", 17 | "native_build": "node-gyp rebuild", 18 | "install": "node install.js" 19 | }, 20 | "dependencies": { 21 | "electron-util": "^0.17.2", 22 | "execa": "^5.1.1", 23 | "macos-version": "^5.2.1" 24 | }, 25 | "devDependencies": { 26 | "electron": "^23.2.2", 27 | "node-abi": "^3.35.0", 28 | "tsd": "^0.28.1", 29 | "xo": "^0.54.1" 30 | }, 31 | "keywords": [ 32 | "macos", 33 | "swift", 34 | "screen", 35 | "recording", 36 | "permission", 37 | "permissions", 38 | "system", 39 | "preferences", 40 | "screen recording", 41 | "catalina", 42 | "electron" 43 | ], 44 | "engines": { 45 | "node": ">=8" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | import {openSystemPreferences as electronUtilOpenSystemPreferences} from 'electron-util'; 5 | 6 | /** 7 | Check whether or not the current app has the required permissions to record the screen 8 | 9 | @example 10 | ``` 11 | const hasPermissions = hasScreenCapturePermission(); // true 12 | ``` 13 | */ 14 | export const hasScreenCapturePermission: () => boolean; 15 | 16 | /** 17 | Check whether or not the current app has already asked for permissions before. 18 | 19 | Only works in Electron apps. Otherwise returns `false`. 20 | 21 | @example 22 | ``` 23 | const hasAsked = hasPromptedForPermission(); // true 24 | ``` 25 | */ 26 | export const hasPromptedForPermission: () => boolean; 27 | 28 | /** 29 | Resets ScreenCapture permissions for all applications. 30 | 31 | Optionally pass in an object with `bundleId` to only reset permissions for that app 32 | 33 | @example 34 | ``` 35 | resetPermissions({bundleId: 'com.googlecode.iterm2'}); // true 36 | ``` 37 | 38 | @returns A boolean that is true if the permissions were reset successfully and false otherwise 39 | */ 40 | export const resetPermissions: (options?: {bundleId?: string}) => boolean; 41 | 42 | /** 43 | Open the System Preferences in the Screen Recording permissions section under the Security pane. 44 | 45 | Only available in Electron apps. 46 | 47 | @example 48 | ``` 49 | openSystemPreferences().then(() => console.log('Opened')); 50 | ``` 51 | 52 | @returns A Promise that resolves when the window is open 53 | */ 54 | export const openSystemPreferences: () => ReturnType; 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const execa = require('execa'); 5 | const {isElectron} = require('electron-util/node'); 6 | const macosVersion = require('macos-version'); 7 | 8 | const permissionExists = macosVersion.isGreaterThanOrEqualTo('10.15'); 9 | 10 | let filePath; 11 | 12 | if (isElectron) { 13 | const {api, openSystemPreferences} = require('electron-util'); 14 | 15 | exports.openSystemPreferences = () => openSystemPreferences('security', 'Privacy_ScreenCapture'); 16 | 17 | filePath = api.app && path.join(api.app.getPath('userData'), '.has-app-requested-screen-capture-permissions'); 18 | } 19 | 20 | exports.hasScreenCapturePermission = () => { 21 | if (!permissionExists) { 22 | return true; 23 | } 24 | 25 | const screenCapturePermission = require('./build/Release/screencapturepermissions.node'); 26 | const hasPermission = screenCapturePermission.hasPermissions(); 27 | 28 | if (!hasPermission && filePath) { 29 | try { 30 | fs.writeFileSync(filePath, ''); 31 | } catch (error) { 32 | if (error.code === 'ENOENT') { 33 | fs.mkdirSync(path.dirname(filePath)); 34 | fs.writeFileSync(filePath, ''); 35 | } 36 | 37 | throw error; 38 | } 39 | } 40 | 41 | return hasPermission; 42 | }; 43 | 44 | exports.hasPromptedForPermission = () => { 45 | if (!permissionExists) { 46 | return false; 47 | } 48 | 49 | if (filePath && fs.existsSync(filePath)) { 50 | return true; 51 | } 52 | 53 | return false; 54 | }; 55 | 56 | exports.resetPermissions = ({bundleId = ''} = {}) => { 57 | try { 58 | execa.sync('tccutil', ['reset', 'ScreenCapture', bundleId].filter(Boolean)); 59 | 60 | if (filePath && fs.existsSync(filePath)) { 61 | fs.unlinkSync(filePath); 62 | } 63 | 64 | return true; 65 | } catch (error) { 66 | console.log(error); 67 | return false; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /screen-capture-permissions.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #include 4 | 5 | static napi_value hasPermissions(napi_env env, napi_callback_info info) { 6 | napi_status status; 7 | bool hasPermissions; 8 | napi_value hasPermissionsInt32; 9 | napi_value ret; 10 | 11 | if (@available(macOS 11.0, *)) { 12 | hasPermissions = CGPreflightScreenCaptureAccess(); 13 | } else { 14 | CGDisplayStreamRef stream_ref; 15 | stream_ref = CGDisplayStreamCreateWithDispatchQueue(CGMainDisplayID(), 16 | 1, 17 | 1, 18 | 'BGRA', 19 | nil, 20 | dispatch_get_main_queue(), 21 | ^(CGDisplayStreamFrameStatus status, 22 | uint64_t time, 23 | IOSurfaceRef frame, 24 | CGDisplayStreamUpdateRef ref) {} 25 | ); 26 | hasPermissions = stream_ref != nil; 27 | } 28 | 29 | status = napi_create_int32(env, hasPermissions, &hasPermissionsInt32); 30 | assert(status == napi_ok); 31 | status = napi_coerce_to_bool(env, hasPermissionsInt32, &ret); 32 | assert(status == napi_ok); 33 | return ret; 34 | } 35 | 36 | #define DECLARE_NAPI_METHOD(name, func) \ 37 | { name, 0, func, 0, 0, 0, napi_default, 0 } 38 | 39 | napi_value Init(napi_env env, napi_value exports) { 40 | napi_status status; 41 | napi_property_descriptor hasPermissionsDescriptor = DECLARE_NAPI_METHOD("hasPermissions", hasPermissions); 42 | status = napi_define_properties(env, exports, 1, &hasPermissionsDescriptor); 43 | assert(status == napi_ok); 44 | return exports; 45 | } 46 | 47 | NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 48 | 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mac-screen-capture-permissions 2 | 3 | > Check and request permission to capture the screen on macOS (introduced with 10.15 Catalina) 4 | 5 | ## Install 6 | 7 | Building the module from source requires macOS 11+ SDK, but the resulting module will still run on <10.15, 10.15 and 11+. 8 | 9 | ``` 10 | $ npm install mac-screen-capture-permissions 11 | ``` 12 | 13 | ## Usage 14 | 15 | See [example.js](https://github.com/karaggeorge/mac-screen-capture-permissions/blob/master/example.js) for usage. 16 | 17 | ```js 18 | const { 19 | hasScreenCapturePermission, 20 | hasPromptedForPermission 21 | } = require('mac-screen-capture-permissions'); 22 | 23 | hasPromptedForPermission(); 24 | 25 | // false 26 | 27 | hasScreenCapturePermission(); 28 | 29 | // false 30 | 31 | hasPromptedForPermission(); 32 | 33 | // true 34 | 35 | 36 | // After accepting the permissions 37 | 38 | hasScreenCapturePermission(); 39 | 40 | // true 41 | ``` 42 | 43 | ## API 44 | 45 | #### `.hasScreenCapturePermission(): boolean` 46 | 47 | Whether or not the current app has the required permissions to record the screen. If this is the first time attempting, a permissions dialog will be shown to the user. Any subsequent calls to `hasScreenCapturePermission` will just check for the permission but won't show a dialog. If the user denied the original request, you need to prompt them to enable the permissions in the System Preferences. 48 | 49 | This can be reset by calling `resetPermissions`. The dialog will be shown again after that. 50 | 51 | Returns `true` on macOS versions older than 10.15 since this permission wasn't present 52 | 53 | #### `.hasPromptedForPermission(): boolean` 54 | 55 | **Note:** Only works for Electron apps 56 | 57 | Whether or not the permission dialog has been shown to the user. Will be `false` if you haven't called `hasScreenCapturePermission` for this app yet, and `true` otherwise. 58 | 59 | This can be reset by calling `resetPermissions`, 60 | 61 | Returns `false` on macOS versions older than 10.15 since this permission wasn't present 62 | 63 | #### `.resetPermissions({bundleId?: string}): boolean` 64 | 65 | Reset the `ScreenCapture` permissions. It will reset the permissions for **all** apps, so use with care. Provide a `bundleId` (i.e. com.apple.Terminal) to reset the permissions only for that app. 66 | 67 | Calls `tccutil reset ScreenCapture [bundleId]`. 68 | 69 | This will revoke access if it was previously granted, and it will trigger the permissions dialog the next time `hasScreenCapturePermission` is called. 70 | 71 | Returns `true` if the command executed successfully and `false` otherwise. 72 | 73 | Returns `false` on macOS versions older than 10.15 since this permission wasn't present 74 | 75 | #### `.openSystemPreferences(): Promise` 76 | 77 | Open the System Preferences in the Screen Recording permissions section under the Security pane. 78 | 79 | Only available in Electron apps. 80 | 81 | Returns a Promise that resolves when the window is opened 82 | 83 | ## License 84 | 85 | MIT 86 | --------------------------------------------------------------------------------