Skip to content

Commit 3de393e

Browse files
committed
feat: add macOS permission APIs.
1 parent 94be6a7 commit 3de393e

5 files changed

Lines changed: 112 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Unreleased
22

3+
* feat: Add macOS Accessibility and Screen Recording permission APIs.
34
* fix: Normalize smooth mouse movement timing on Windows.
45
* fix: Write the complete BMP file size in serialized headers.
56
* test: Adopt Target Practice 0.0.8 lifecycle and verify fixture visibility and interactivity.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ Read the [Wiki](https://github.com/octalmage/robotjs/wiki) for more information!
106106

107107
The RobotJS API is hosted at <https://robotjs.dev/docs/syntax>.
108108

109+
### macOS permissions
110+
111+
`getAccessibilityPermission()` and `getScreenCapturePermission()` report the
112+
current grants. `requestAccessibilityPermission()` and
113+
`requestScreenCapturePermission()` trigger the corresponding macOS system
114+
prompts. macOS still requires the user to approve each request.
115+
The Accessibility prompt is asynchronous, so `requestAccessibilityPermission()`
116+
returns the current grant. Check `getAccessibilityPermission()` again after the
117+
user responds.
118+
109119
## Building
110120

111121
Please ensure you have the required dependencies before installing:

index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export function getMousePos(): { x: number, y: number }
9595
export function getPixelColor(x: number, y: number): string
9696
export function getScreenSize(): { width: number, height: number }
9797
export function getDisplays(): Display[]
98+
export function getAccessibilityPermission(): boolean | null
99+
export function requestAccessibilityPermission(): boolean | null
100+
export function getScreenCapturePermission(): boolean | null
101+
export function requestScreenCapturePermission(): boolean | null
98102

99103
export var screen: Screen
100104
export var image: ImageModule

src/robotjs.cc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
#include "io.h"
1616
#include "snprintf.h"
1717
#include "microsleep.h"
18+
#if defined(__APPLE__)
19+
#include <ApplicationServices/ApplicationServices.h>
20+
#endif
1821
#if defined(USE_X11)
1922
#include "xdisplay.h"
2023
#endif
@@ -1747,6 +1750,62 @@ Napi::Value saveImageWrapper(const Napi::CallbackInfo& info)
17471750
return Napi::Boolean::New(env, true);
17481751
}
17491752

1753+
Napi::Value getAccessibilityPermissionWrapper(const Napi::CallbackInfo& info)
1754+
{
1755+
Napi::Env env = info.Env();
1756+
1757+
#if defined(__APPLE__)
1758+
return Napi::Boolean::New(env, AXIsProcessTrusted());
1759+
#else
1760+
return env.Null();
1761+
#endif
1762+
}
1763+
1764+
Napi::Value requestAccessibilityPermissionWrapper(const Napi::CallbackInfo& info)
1765+
{
1766+
Napi::Env env = info.Env();
1767+
1768+
#if defined(__APPLE__)
1769+
const void *keys[] = { kAXTrustedCheckOptionPrompt };
1770+
const void *values[] = { kCFBooleanTrue };
1771+
CFDictionaryRef options = CFDictionaryCreate(
1772+
kCFAllocatorDefault,
1773+
keys,
1774+
values,
1775+
1,
1776+
&kCFTypeDictionaryKeyCallBacks,
1777+
&kCFTypeDictionaryValueCallBacks
1778+
);
1779+
const bool trusted = AXIsProcessTrustedWithOptions(options);
1780+
CFRelease(options);
1781+
return Napi::Boolean::New(env, trusted);
1782+
#else
1783+
return env.Null();
1784+
#endif
1785+
}
1786+
1787+
Napi::Value getScreenCapturePermissionWrapper(const Napi::CallbackInfo& info)
1788+
{
1789+
Napi::Env env = info.Env();
1790+
1791+
#if defined(__APPLE__)
1792+
return Napi::Boolean::New(env, CGPreflightScreenCaptureAccess());
1793+
#else
1794+
return env.Null();
1795+
#endif
1796+
}
1797+
1798+
Napi::Value requestScreenCapturePermissionWrapper(const Napi::CallbackInfo& info)
1799+
{
1800+
Napi::Env env = info.Env();
1801+
1802+
#if defined(__APPLE__)
1803+
return Napi::Boolean::New(env, CGRequestScreenCaptureAccess());
1804+
#else
1805+
return env.Null();
1806+
#endif
1807+
}
1808+
17501809
Napi::Object InitAll(Napi::Env env, Napi::Object exports)
17511810
{
17521811
exports.Set(Napi::String::New(env, "dragMouse"),
@@ -1842,6 +1901,18 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports)
18421901
exports.Set(Napi::String::New(env, "setXDisplayName"),
18431902
Napi::Function::New(env, setXDisplayNameWrapper));
18441903

1904+
exports.Set(Napi::String::New(env, "getAccessibilityPermission"),
1905+
Napi::Function::New(env, getAccessibilityPermissionWrapper));
1906+
1907+
exports.Set(Napi::String::New(env, "requestAccessibilityPermission"),
1908+
Napi::Function::New(env, requestAccessibilityPermissionWrapper));
1909+
1910+
exports.Set(Napi::String::New(env, "getScreenCapturePermission"),
1911+
Napi::Function::New(env, getScreenCapturePermissionWrapper));
1912+
1913+
exports.Set(Napi::String::New(env, "requestScreenCapturePermission"),
1914+
Napi::Function::New(env, requestScreenCapturePermissionWrapper));
1915+
18451916
return exports;
18461917
}
18471918

test/permissions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const robot = require('../index.js');
2+
3+
describe('Permissions', () => {
4+
it('Reports permission status without prompting.', () => {
5+
const accessibility = robot.getAccessibilityPermission();
6+
const screenCapture = robot.getScreenCapturePermission();
7+
8+
if (process.platform === 'darwin') {
9+
expect(typeof accessibility).toBe('boolean');
10+
expect(typeof screenCapture).toBe('boolean');
11+
} else {
12+
expect(accessibility).toBeNull();
13+
expect(screenCapture).toBeNull();
14+
}
15+
});
16+
17+
it('Exposes explicit permission request functions.', () => {
18+
expect(typeof robot.requestAccessibilityPermission).toBe('function');
19+
expect(typeof robot.requestScreenCapturePermission).toBe('function');
20+
21+
if (process.platform !== 'darwin') {
22+
expect(robot.requestAccessibilityPermission()).toBeNull();
23+
expect(robot.requestScreenCapturePermission()).toBeNull();
24+
}
25+
});
26+
});

0 commit comments

Comments
 (0)