Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to turn on/off "do not disturb" for OS X programmatically

Tags:

macos

cocoa

Is it possible to turn on/off "Do no disturb" for mac os x programmatically, which means by code. I've done some research by google, such as:

  1. By Automator script applescripting notification center scheduling do not disturb. By the way I don't make it work, when I killall NotificationCenter, the Do not disturb switch is still off

  2. Writing defaults by code, programmatic equivalent of defaults write command e.g. how to use NSUserDefaults, however how to work with the args -currentHost (mentioned in the article in link above)

like image 922
xmkevinchen Avatar asked Aug 08 '14 18:08

xmkevinchen


2 Answers

Unfortunatelly (yet unsurprisingly), there's no public API for dealing with user’s notifications preferences, one of which is Do Not Disturb mode (DND).

Nevertheless if your want to provide the feature of turning DND on and off in your application, you're not out of luck actually: there're three ways for you to choose from.

#1. Run This Tiny AppleScript and See The Results

Here's an AppleScript by philastokes which takes into account that option-clicking on the Notification Center icon in menubar does exactly what we want: it toggles the DND mode!

(* Copyright © philastokes from applehelpwriter.com *)
(* Link: http://applehelpwriter.com/2014/12/10/applescript-toggle-notification-centre-yosemite *)

tell application "System Events"
    tell application process "SystemUIServer"
        try
            (* Replace "Notification Center" with "NotificationCenter"
            here if you're targeting OS X 10.10 *)
            if exists menu bar item "Notification Center, Do Not Disturb enabled" of menu bar 2 then
                key down option
                (* Replace "Notification Center" with "NotificationCenter"
                here if you're targeting OS X 10.10 *)
                click menu bar item "Notification Center, Do Not Disturb enabled" of menu bar 2
                key up option
            else
                key down option
                click menu bar item "Notification Center" of menu bar 2
                key up option
            end if
        on error
            key up option
        end try
    end tell
end tell

Please note that you need to replace "Notification Center" with "NotificationCenter" everywhere if you're targeting OS X 10.10

Also, executing this code requires your application to have Accessibility enabled for it.

One last step is to wrap it into Objctive-C/Swift code:

NSString *source  = ... // the AppleScript code
NSAppleScript *script = [[NSAppleScript alloc] initWithSource: source];
NSDictionary *errorInfo = nil;
[script executeAndReturnError: &errorInfo];

#2. Use Accessibility API directly

Instead of letting AppleScript engine to deal with user interactions, we could make them ourselvs using Accessibility API available in the system:

Executing this code requires your application to have Accessibility enabled for it.

pid_t SystemUIServerPID = [[NSRunningApplication runningApplicationsWithBundleIdentifier:
                              @"com.apple.systemuiserver"].firstObject processIdentifier];
assert(SystemUIServerPID != 0);

AXUIElementRef target = AXUIElementCreateApplication(SystemUIServerPID);
assert(target != nil);

CFArrayRef attributes = nil;
AXUIElementCopyAttributeNames(target, &attributes);
assert([(__bridge NSArray *)attributes containsObject: @"AXExtrasMenuBar"]);

CFTypeRef menubar;
AXUIElementCopyAttributeValue(target, CFSTR("AXExtrasMenuBar"), &menubar);

CFTypeRef children;
AXUIElementCopyAttributeValue(menubar, CFSTR("AXChildren"), &children);

// XXX: I hate mixing CF and Objective-C like this but it's just a PoC code.
// Anyway, I'm sorry
NSArray *items = (__bridge NSArray *)children;
for (id x in items) {
    AXUIElementRef child = (__bridge AXUIElementRef)x;
    CFTypeRef title;
    AXUIElementCopyAttributeValue(child, CFSTR("AXTitle"), &title);
    assert(CFGetTypeID(title) == CFStringGetTypeID());
    // XXX: the proper check would be to match the whole "Notification Center" string,
    // but on OS X 10.10 it's "NotificationCenter" (without the space in-between) and
    // I don't feel like having two conditionals here
    if (CFStringHasPrefix(title, CFSTR("Notification"))) {
        optionKeyDown();
        AXUIElementPerformAction(child, kAXPressAction);
        optionKeyUp();
        break;
    }
}

where optionKeyDown() and optionKeyUp() are

#define kOptionKeyCode (58)

static void optionKeyDown(void)
{
    CGEventRef e = CGEventCreateKeyboardEvent(NULL, kOptionKeyCode, true);
    CGEventPost(kCGSessionEventTap, e);
    CFRelease(e);
}

static void optionKeyUp(void)
{
    CGEventRef e = CGEventCreateKeyboardEvent(NULL, kOptionKeyCode, false);
    CGEventPost(kCGSessionEventTap, e);
    CFRelease(e);
}

#3. Let's pretent we're Notifications.prefPane

You may've noticed that you could enable DND mode via Notifications preferences pane by setting the mode's range from 00:00 to 23:59. And disabling DND would be just unchecking the checkbox.

Here's what's inside Notifications.prefPane:

void turnDoNotDisturbOn(void)
{
    // The trick is to set DND time range from 00:00 (0 minutes) to 23:59 (1439 minutes),
    // so it will always be on
    CFPreferencesSetValue(CFSTR("dndStart"), (__bridge CFPropertyListRef)(@(0.0f)),
                          CFSTR("com.apple.notificationcenterui"),
                          kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    CFPreferencesSetValue(CFSTR("dndEnd"), (__bridge CFPropertyListRef)(@(1440.f)),
                          CFSTR("com.apple.notificationcenterui"),
                          kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    CFPreferencesSetValue(CFSTR("doNotDisturb"), (__bridge CFPropertyListRef)(@(YES)),
                          CFSTR("com.apple.notificationcenterui"),
                          kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    // Notify all the related daemons that we have changed Do Not Disturb preferences
    commitDoNotDisturbChanges();
}


void turnDoNotDisturbOff()
{
    CFPreferencesSetValue(CFSTR("dndStart"), NULL,
                        CFSTR("com.apple.notificationcenterui"),
                        kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    CFPreferencesSetValue(CFSTR("dndEnd"), NULL,
                          CFSTR("com.apple.notificationcenterui"),
                          kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    CFPreferencesSetValue(CFSTR("doNotDisturb"), (__bridge CFPropertyListRef)(@(NO)),
                          CFSTR("com.apple.notificationcenterui"),
                          kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);

    commitDoNotDisturbChanges();
}

void commitDoNotDisturbChanges(void)
{
    /// XXX: I'm using kCFPreferencesCurrentUser placeholder here which means that this code must
    /// be run under regular user's account (not root/admin). If you're going to run this code
    /// from a privileged helper, use kCFPreferencesAnyUser in order to toggle DND for all users
    /// or drop privileges and use kCFPreferencesCurrentUser.
    CFPreferencesSynchronize(CFSTR("com.apple.notificationcenterui"), kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
    [[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"com.apple.notificationcenterui.dndprefs_changed"
                                                               object: nil userInfo: nil
                                                   deliverImmediately: YES];
}
like image 99
Dmitry Rodionov Avatar answered Oct 22 '22 01:10

Dmitry Rodionov


@Dmitry's solution No. 2 was the only one that worked correctly for me.

Here is a Swift 4 version of it:

    func enableDND(){

        CFPreferencesSetValue("dndStart" as CFString, CGFloat(0) as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)

        CFPreferencesSetValue("dndEnd" as CFString, CGFloat(1440) as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)

        CFPreferencesSetValue("doNotDisturb" as CFString, true as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)


        commitDNDChanges()
    }

    func disableDND(){
        CFPreferencesSetValue("dndStart" as CFString, nil, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)

        CFPreferencesSetValue("dndEnd" as CFString, nil, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)

        CFPreferencesSetValue("doNotDisturb" as CFString, false as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
        commitDNDChanges()
    }

    func commitDNDChanges(){
        CFPreferencesSynchronize("com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
        DistributedNotificationCenter.default().postNotificationName(NSNotification.Name(rawValue: "com.apple.notificationcenterui.dndprefs_changed"), object: nil, userInfo: nil, deliverImmediately: true)
    }
like image 14
arnoapp Avatar answered Oct 22 '22 01:10

arnoapp