How to activate DND automatically?

I use a MacBook Pro at work which is most of the time connected to a docking station via a lightning cable at my desk. There I usually turn off do not disturb (DND) mode and activate screensaver/screenlock after 2 minutes. When I am at meetings it happens regularly that I want to present something but then it is annoying if notifications are displayed or the screensaver turns on after 2 minutes. How can I automatically turn on DND mode and turn off screensaver automatically whenever my MacBook is not connected to power (docking station)?


The only way I could get this to work is by building a small Swift command-line application. It works pretty well, with two major caveats:

  1. You cannot have Do Not Disturb scheduled to turn on and off at certain times (check System Preferences)
  2. The menu bar item always looks as though Do Not Disturb is enabled, even though it is not

However, if these caveats aren't a huge deal, the app does everything else you need: it prevents the display from sleeping and enables Do Not Disturb when on battery, and reverses those settings when you connect to AC power. Here is the (slightly involved) process for creating this app:

  • Install Xcode from the Mac App Store if you have not already done so
  • Open Xcode and install any necessary components as prompted
  • From the "Welcome to Xcode" window, select Create a New Xcode Project Welcome to Xcode panel
  • Select macOS at the top of the dropdown that appears, then select Command Line Tool under the Application header and click Next New project type selector
  • Enter something for your product name—which is the name that will appear on your app (I'm going to be using BatteryDetector)—and fill in the other fields (they don't really matter); make sure the language is set to Swift! Project options panel
  • Select somewhere on your computer to save the project (wherever you want), uncheck the box for creating a Git repository, and click Create Source control and group options
  • Once your project is created (a new window will open), select File > New > File… or press ⌘N to open the New File dialog
  • Select Header File as the file type and click Next Header file type selection
  • Name your file [your project name]-Bridging-Header.h, substituting your actual project name for [your-project-name]. In my example, the name would be BatteryDetector-Bridging-Header.h.
  • Make sure that the Group at the bottom of the dialog has a yellow folder icon next to it and says the name of your project. If it has a blue icon or something else, change it to the option that has a yellow folder icon and the name of your project. See the screenshot below for the correct configuration. When everything is correct, click Create. File group and location dialog
  • Once the file is created, it should automatically be opened. Once it is, erase all of the auto-generated contents and paste in the following code. The code below should be the only code in the file:

    #import <IOKit/ps/IOPowerSources.h>
    
  • Next, select the project at the top of the hierarchy in the top left of the window, then select the target under the Targets list, select Build Settings at the top of the main panel, and ensure All is selected beneath that (see screenshot below) Build settings screen

  • Use the search box in the top right of the Build Settings screen to search for objective-c bridging header. You should see a setting called Objective-C Bridging Header come to the top of the list.
  • Double-click to the right of the Objective-C Bridging Header label so that a text box appears, and enter [your-project-name]/[your-project-name]-Bridging-Header.h in that text box, replacing [your-project-name] with your project's actual name. In my example, I would enter BatteryDetector/BatteryDetector-Bridging-Header.h. Then press return. Objective-C Bridging Header build setting
  • Next, click on the main.swift file in the file tree on the left. Delete all of the code it contains, and paste in the code below. The following code should be the only code in main.swift:

    import Cocoa
    
    var context = 0
    var proc: Process!
    
    func enableDND() {
            CFPreferencesSetValue("doNotDisturb" as CFString, true as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
            CFPreferencesSetValue("doNotDisturbDate" as CFString, Date() as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
            commitDNDChanges()
    }
    
    func disableDND() {
            CFPreferencesSetValue("doNotDisturb" as CFString, false as CFPropertyList, "com.apple.notificationcenterui" as CFString, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
            CFPreferencesSetValue("doNotDisturbDate" as CFString, nil, "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)
            NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.notificationcenterui").first?.terminate()
    }
    
    func startCaffeinate() {
            proc = Process()
            proc.launchPath = "/usr/bin/caffeinate"
            proc.arguments = ["-d", "-i"]
            proc.launch()
    }
    
    func stopCaffeinate() {
            proc.terminate()
    }
    
    let loop = IOPSNotificationCreateRunLoopSource({(context: UnsafeMutableRawPointer?) in
            let snap = IOPSCopyPowerSourcesInfo().takeRetainedValue() as CFTypeRef
            let source = IOPSGetProvidingPowerSourceType(snap).takeRetainedValue() as String
            if source == "Battery Power" {
                    enableDND()
                    startCaffeinate()
            } else {
                    disableDND()
                    stopCaffeinate()
            }
    }, &context).takeRetainedValue() as CFRunLoopSource
    
    CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, .defaultMode)
    
    RunLoop.main.run()
    
  • Next, export the application by selecting Product > Archive in the menu bar. Once the archive finishes, a window will appear with an Export button. Click Export, select Built Products, and click Next. Archive screen

  • Select a location where you will save the exported executable on your disk, then click Export.
  • Now, navigate to the folder you just saved to your disk (called [your-project-name] [today's-date]) and navigate through all of the subfolders (probably usr, local, bin) until you find the executable called BatteryDetector (or whatever you named your project). You can store this executable anywhere on disk, but it should be at a stable location (i.e., a location from which you don't plan on moving it) or your computer won't be able to locate it.
  • Next, copy the following text and paste it into a text-editing program like TextEdit or a code editor (make sure that it won't convert quotes to "smart quotes"). Replace the text [your-executable-location] with the full path to where your executable is stored on disk (e.g., /Applications/BatteryDetector).

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
      <dict>
            <key>Label</key>
            <string>com.launchagent.batterydetector</string>
            <key>Program</key>
            <string>[your-executable-location]</string>
            <key>RunAtLoad</key>
            <true/>
      </dict>
    </plist>
    
  • Once you have entered the correct path, copy the newly-modified text (i.e., with your corrected path) to your clipboard.

  • Next, open Terminal (located at /Applications/Utilities/Terminal.app).
  • Type the following into Terminal, exactly as it is written here, and then press return. Do not copy and paste it—the command will only work if the text you just modified is currently on your clipboard: test -e ~/Library/LaunchAgents || mkdir ~/Library/LaunchAgents; pbpaste > ~/Library/LaunchAgents/com.launchagent.batterydetector
  • Finally, log out and then back in to start the app running. It will continue to run in the background, and will automatically start each time you log in. If you need to quit it for some reason, use Activity Monitor and search for the name of your executable (e.g., BatteryDetector).

Here are some sources with more information about some of the code I'm using:

  • Create a CFRunLoopSourceRef using IOPSNotificationCreateRunLoopSource in Swift
  • sindresorhus' do-not-disturb on GitHub