Skip to content

Commit

Permalink
Observe changes in accessibility access continuously
Browse files Browse the repository at this point in the history
  • Loading branch information
emreyolcu committed May 31, 2024
1 parent a59f543 commit cf50b14
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 36 deletions.
8 changes: 4 additions & 4 deletions DiscreteScroll.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,14 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 3;
INFOPLIST_FILE = DiscreteScroll/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.9;
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll;
PRODUCT_NAME = "$(TARGET_NAME)";
};
Expand All @@ -267,14 +267,14 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 3;
INFOPLIST_FILE = DiscreteScroll/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.9;
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.emreyolcu.DiscreteScroll;
PRODUCT_NAME = "$(TARGET_NAME)";
};
Expand Down
87 changes: 62 additions & 25 deletions DiscreteScroll/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
#define DEFAULT_LINES 3
#define SIGN(x) (((x) > 0) - ((x) < 0))

static const CFStringRef AX_NOTIFICATION = CFSTR("com.apple.accessibility.api");
static bool TRUSTED;

static CFMachPortRef TAP;
static CFRunLoopSourceRef SOURCE;

static int LINES;

CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo)
static CGEventRef tapCallback(CGEventTapProxy proxy,
CGEventType type, CGEventRef event, void *userInfo)
{
if (CGEventGetIntegerValueField(event, kCGScrollWheelEventIsContinuous) == 0) {
int delta = (int)CGEventGetIntegerValueField(event, kCGScrollWheelEventPointDeltaAxis1);
Expand All @@ -15,7 +22,7 @@ CGEventRef callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, v
return event;
}

void displayNoticeAndExit(CFStringRef alertHeader)
static void displayNoticeAndExit(CFStringRef alertHeader)
{
CFUserNotificationDisplayNotice(
0, kCFUserNotificationCautionAlertLevel,
Expand All @@ -26,44 +33,74 @@ void displayNoticeAndExit(CFStringRef alertHeader)
exit(EXIT_FAILURE);
}

static void notificationCallback(CFNotificationCenterRef center, void *observer,
CFNotificationName name, const void *object,
CFDictionaryRef userInfo)
{
if (CFStringCompare(name, AX_NOTIFICATION, 0) == kCFCompareEqualTo) {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopPerformBlock(
runLoop, kCFRunLoopDefaultMode, ^{
bool previouslyTrusted = TRUSTED;
if ((TRUSTED = AXIsProcessTrusted()) != previouslyTrusted) {
CFRunLoopStop(runLoop);
if (SOURCE && CFRunLoopContainsSource(runLoop, SOURCE, kCFRunLoopDefaultMode)) {
CGEventTapEnable(TAP, TRUSTED);
CFRunLoopRun();
} else if (!TRUSTED) {
CFRunLoopRun();
}
}
}
);
}
}

static bool getIntPreference(CFStringRef key, int *valuePtr)
{
CFNumberRef number = (CFNumberRef)CFPreferencesCopyAppValue(
key, kCFPreferencesCurrentApplication
);
bool got = false;
if (number) {
if (CFGetTypeID(number) == CFNumberGetTypeID())
got = CFNumberGetValue(number, kCFNumberIntType, valuePtr);
CFRelease(number);
}

return got;
}

int main(void)
{
CFNotificationCenterAddObserver(
CFNotificationCenterGetDistributedCenter(), NULL,
notificationCallback, AX_NOTIFICATION, NULL,
CFNotificationSuspensionBehaviorDeliverImmediately
);
CFDictionaryRef options = CFDictionaryCreate(
kCFAllocatorDefault,
(const void **)&kAXTrustedCheckOptionPrompt, (const void **)&kCFBooleanTrue, 1,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks
);
bool trusted = AXIsProcessTrustedWithOptions(options);
TRUSTED = AXIsProcessTrustedWithOptions(options);
CFRelease(options);
if (!trusted)
displayNoticeAndExit(
CFSTR("Restart DiscreteScroll after granting it access to accessibility features.")
);
if (!TRUSTED)
CFRunLoopRun();

CFNumberRef value = (CFNumberRef)CFPreferencesCopyAppValue(
CFSTR("lines"), kCFPreferencesCurrentApplication
);
bool got = false;
if (value) {
if (CFGetTypeID(value) == CFNumberGetTypeID())
got = CFNumberGetValue(value, kCFNumberIntType, &LINES);
CFRelease(value);
}
if (!got)
if (!getIntPreference(CFSTR("lines"), &LINES))
LINES = DEFAULT_LINES;

CFMachPortRef tap = CGEventTapCreate(
TAP = CGEventTapCreate(
kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventScrollWheel), callback, NULL
CGEventMaskBit(kCGEventScrollWheel), tapCallback, NULL
);
if (!tap)
if (!TAP)
displayNoticeAndExit(CFSTR("DiscreteScroll could not create an event tap."));
CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
if (!source)
SOURCE = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, TAP, 0);
if (!SOURCE)
displayNoticeAndExit(CFSTR("DiscreteScroll could not create a run loop source."));
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(tap);
CFRelease(source);
CFRunLoopAddSource(CFRunLoopGetCurrent(), SOURCE, kCFRunLoopDefaultMode);
CFRunLoopRun();

return EXIT_SUCCESS;
Expand Down
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,28 @@ As of May 2024, this application works on macOS versions 10.9–14.0.

### Installation

You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.0.1/DiscreteScroll.zip).

It needs to be run each time you boot.
If you want this to be automatic, do the following:
You may download the binary [here](https://github.com/emreyolcu/discrete-scroll/releases/download/v1.1.0/DiscreteScroll.zip).
DiscreteScroll requires access to accessibility features.
Upon startup, if it does not have access, it will prompt you and wait.
You do not need to restart the application
after you grant it access to accessibility features.

> [!CAUTION]
> You may safely toggle accessibility access
> for DiscreteScroll while it is running.
> *However, you should not remove it from the list of trusted applications
> while it is running without first unchecking the box next to its name.
> Otherwise, your mouse might become unresponsive.*
If you want the application to run automatically when you log in,
do the following:

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

If you want to quit the application, do the following:
If you want to quit the application, either run `killall DiscreteScroll`
or do the following:

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

If you set the key `lines` to some value other than an integer,
the default value of 3 is used as a fallback.
> [!WARNING]
> If you set `lines` to some value other than an integer,
> then the default value of 3 is used as a fallback.
You should restart the application for the setting to take effect.

### Uninstallation

To uninstall DiscreteScroll, quit the application, move it to trash,
and remove it from the lists for accessibility access and login items.
You can remove any stored preferences by running the following:

```
defaults delete com.emreyolcu.DiscreteScroll
```

### Potential problems

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

0 comments on commit cf50b14

Please sign in to comment.