Skip to content

Commit cf50b14

Browse files
committed
Observe changes in accessibility access continuously
1 parent a59f543 commit cf50b14

3 files changed

Lines changed: 96 additions & 36 deletions

File tree

DiscreteScroll.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,14 @@
249249
buildSettings = {
250250
CODE_SIGN_STYLE = Automatic;
251251
COMBINE_HIDPI_IMAGES = YES;
252-
CURRENT_PROJECT_VERSION = 2;
252+
CURRENT_PROJECT_VERSION = 3;
253253
INFOPLIST_FILE = DiscreteScroll/Info.plist;
254254
LD_RUNPATH_SEARCH_PATHS = (
255255
"$(inherited)",
256256
"@executable_path/../Frameworks",
257257
);
258258
MACOSX_DEPLOYMENT_TARGET = 10.9;
259-
MARKETING_VERSION = 1.0.1;
259+
MARKETING_VERSION = 1.1.0;
260260
PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll;
261261
PRODUCT_NAME = "$(TARGET_NAME)";
262262
};
@@ -267,14 +267,14 @@
267267
buildSettings = {
268268
CODE_SIGN_STYLE = Automatic;
269269
COMBINE_HIDPI_IMAGES = YES;
270-
CURRENT_PROJECT_VERSION = 2;
270+
CURRENT_PROJECT_VERSION = 3;
271271
INFOPLIST_FILE = DiscreteScroll/Info.plist;
272272
LD_RUNPATH_SEARCH_PATHS = (
273273
"$(inherited)",
274274
"@executable_path/../Frameworks",
275275
);
276276
MACOSX_DEPLOYMENT_TARGET = 10.9;
277-
MARKETING_VERSION = 1.0.1;
277+
MARKETING_VERSION = 1.1.0;
278278
PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll;
279279
PRODUCT_NAME = "$(TARGET_NAME)";
280280
};

DiscreteScroll/main.c

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
#define DEFAULT_LINES 3
44
#define SIGN(x) (((x) > 0) - ((x) < 0))
55

6+
static const CFStringRef AX_NOTIFICATION = CFSTR("com.apple.accessibility.api");
7+
static bool TRUSTED;
8+
9+
static CFMachPortRef TAP;
10+
static CFRunLoopSourceRef SOURCE;
11+
612
static int LINES;
713

8-
CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo)
14+
static CGEventRef tapCallback(CGEventTapProxy proxy,
15+
CGEventType type, CGEventRef event, void *userInfo)
916
{
1017
if (CGEventGetIntegerValueField(event, kCGScrollWheelEventIsContinuous) == 0) {
1118
int delta = (int)CGEventGetIntegerValueField(event, kCGScrollWheelEventPointDeltaAxis1);
@@ -15,7 +22,7 @@ CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, v
1522
return event;
1623
}
1724

18-
void displayNoticeAndExit(CFStringRef alertHeader)
25+
static void displayNoticeAndExit(CFStringRef alertHeader)
1926
{
2027
CFUserNotificationDisplayNotice(
2128
0, kCFUserNotificationCautionAlertLevel,
@@ -26,44 +33,74 @@ void displayNoticeAndExit(CFStringRef alertHeader)
2633
exit(EXIT_FAILURE);
2734
}
2835

36+
static void notificationCallback(CFNotificationCenterRef center, void *observer,
37+
CFNotificationName name, const void *object,
38+
CFDictionaryRef userInfo)
39+
{
40+
if (CFStringCompare(name, AX_NOTIFICATION, 0) == kCFCompareEqualTo) {
41+
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
42+
CFRunLoopPerformBlock(
43+
runLoop, kCFRunLoopDefaultMode, ^{
44+
bool previouslyTrusted = TRUSTED;
45+
if ((TRUSTED = AXIsProcessTrusted()) != previouslyTrusted) {
46+
CFRunLoopStop(runLoop);
47+
if (SOURCE && CFRunLoopContainsSource(runLoop, SOURCE, kCFRunLoopDefaultMode)) {
48+
CGEventTapEnable(TAP, TRUSTED);
49+
CFRunLoopRun();
50+
} else if (!TRUSTED) {
51+
CFRunLoopRun();
52+
}
53+
}
54+
}
55+
);
56+
}
57+
}
58+
59+
static bool getIntPreference(CFStringRef key, int *valuePtr)
60+
{
61+
CFNumberRef number = (CFNumberRef)CFPreferencesCopyAppValue(
62+
key, kCFPreferencesCurrentApplication
63+
);
64+
bool got = false;
65+
if (number) {
66+
if (CFGetTypeID(number) == CFNumberGetTypeID())
67+
got = CFNumberGetValue(number, kCFNumberIntType, valuePtr);
68+
CFRelease(number);
69+
}
70+
71+
return got;
72+
}
73+
2974
int main(void)
3075
{
76+
CFNotificationCenterAddObserver(
77+
CFNotificationCenterGetDistributedCenter(), NULL,
78+
notificationCallback, AX_NOTIFICATION, NULL,
79+
CFNotificationSuspensionBehaviorDeliverImmediately
80+
);
3181
CFDictionaryRef options = CFDictionaryCreate(
3282
kCFAllocatorDefault,
3383
(const void **)&kAXTrustedCheckOptionPrompt, (const void **)&kCFBooleanTrue, 1,
3484
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks
3585
);
36-
bool trusted = AXIsProcessTrustedWithOptions(options);
86+
TRUSTED = AXIsProcessTrustedWithOptions(options);
3787
CFRelease(options);
38-
if (!trusted)
39-
displayNoticeAndExit(
40-
CFSTR("Restart DiscreteScroll after granting it access to accessibility features.")
41-
);
88+
if (!TRUSTED)
89+
CFRunLoopRun();
4290

43-
CFNumberRef value = (CFNumberRef)CFPreferencesCopyAppValue(
44-
CFSTR("lines"), kCFPreferencesCurrentApplication
45-
);
46-
bool got = false;
47-
if (value) {
48-
if (CFGetTypeID(value) == CFNumberGetTypeID())
49-
got = CFNumberGetValue(value, kCFNumberIntType, &LINES);
50-
CFRelease(value);
51-
}
52-
if (!got)
91+
if (!getIntPreference(CFSTR("lines"), &LINES))
5392
LINES = DEFAULT_LINES;
5493

55-
CFMachPortRef tap = CGEventTapCreate(
94+
TAP = CGEventTapCreate(
5695
kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
57-
CGEventMaskBit(kCGEventScrollWheel), callback, NULL
96+
CGEventMaskBit(kCGEventScrollWheel), tapCallback, NULL
5897
);
59-
if (!tap)
98+
if (!TAP)
6099
displayNoticeAndExit(CFSTR("DiscreteScroll could not create an event tap."));
61-
CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
62-
if (!source)
100+
SOURCE = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, TAP, 0);
101+
if (!SOURCE)
63102
displayNoticeAndExit(CFSTR("DiscreteScroll could not create a run loop source."));
64-
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
65-
CFRelease(tap);
66-
CFRelease(source);
103+
CFRunLoopAddSource(CFRunLoopGetCurrent(), SOURCE, kCFRunLoopDefaultMode);
67104
CFRunLoopRun();
68105

69106
return EXIT_SUCCESS;

README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,28 @@ As of May 2024, this application works on macOS versions 10.9–14.0.
1414

1515
### Installation
1616

17-
You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.0.1/DiscreteScroll.zip).
18-
19-
It needs to be run each time you boot.
20-
If you want this to be automatic, do the following:
17+
You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.1.0/DiscreteScroll.zip).
18+
DiscreteScroll requires access to accessibility features.
19+
Upon startup, if it does not have access, it will prompt you and wait.
20+
You do not need to restart the application
21+
after you grant it access to accessibility features.
22+
23+
> [!CAUTION]
24+
> You may safely toggle accessibility access
25+
> for DiscreteScroll while it is running.
26+
> *However, you should not remove it from the list of trusted applications
27+
> while it is running without first unchecking the box next to its name.
28+
> Otherwise, your mouse might become unresponsive.*
29+
30+
If you want the application to run automatically when you log in,
31+
do the following:
2132

2233
1. On macOS 13.0 and later, go to `System Settings > General > Login Items`;
2334
otherwise, go to `System Preferences > Users & Groups > Login Items`.
2435
2. Add `DiscreteScroll` to the list.
2536

26-
If you want to quit the application, do the following:
37+
If you want to quit the application, either run `killall DiscreteScroll`
38+
or do the following:
2739

2840
1. Launch `Activity Monitor`.
2941
2. Search for `DiscreteScroll` and select it.
@@ -40,11 +52,22 @@ This number may even be negative, which inverts scrolling direction.
4052
defaults write com.emreyolcu.DiscreteScroll lines -int LINES
4153
```
4254

43-
If you set the key `lines` to some value other than an integer,
44-
the default value of 3 is used as a fallback.
55+
> [!WARNING]
56+
> If you set `lines` to some value other than an integer,
57+
> then the default value of 3 is used as a fallback.
4558
4659
You should restart the application for the setting to take effect.
4760

61+
### Uninstallation
62+
63+
To uninstall DiscreteScroll, quit the application, move it to trash,
64+
and remove it from the lists for accessibility access and login items.
65+
You can remove any stored preferences by running the following:
66+
67+
```
68+
defaults delete com.emreyolcu.DiscreteScroll
69+
```
70+
4871
### Potential problems
4972

5073
Recent versions of macOS have made it difficult to run unsigned binaries.

0 commit comments

Comments
 (0)