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:
- You cannot have Do Not Disturb scheduled to turn on and off at certain times (check System Preferences)
- 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
- Select
macOS
at the top of the dropdown that appears, then selectCommand Line Tool
under theApplication
header and clickNext
- 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 toSwift
! - Select somewhere on your computer to save the project (wherever you want), uncheck the box for creating a Git repository, and click
Create
- 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 clickNext
- Name your file
[your project name]-Bridging-Header.h
, substituting your actual project name for[your-project-name]
. In my example, the name would beBatteryDetector-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, clickCreate
. -
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, selectBuild Settings
at the top of the main panel, and ensureAll
is selected beneath that (see screenshot below)- 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 calledObjective-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 enterBatteryDetector/BatteryDetector-Bridging-Header.h
. Then press return. -
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 inmain.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 anExport
button. ClickExport
, selectBuilt Products
, and clickNext
.- 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 (probablyusr
,local
,bin
) until you find the executable calledBatteryDetector
(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