Follow the sun

I created a small cli tool called Follow the sun. What it does is switch display settings to Dark mode automatically after sunset, and back to Light mode after sunrise. The idea is that Follow the sun runs as a daemon via launchctl.

For me this was mainly an experiement, it helped me to learn more about:

  • Accessing private frameworks in macOS
  • Using CoreFoundation in C
  • Using macOS Objective-C runtime library (objc_msgSend, sel_registerName, etc) in C.

How to retrieve sunrise/sunset info in macOS

Sunrise/Sunset info is provided by a private macOS system framework named CoreBrightness. A bit more info on this undocumented API (Objective-C) can be found here:

Example output from this API:

    isDaylight = 0;
    nextSunrise = "2018-10-07 05:55:03 +0000";
    nextSunset = "2018-10-06 16:59:34 +0000";
    previousSunrise = "2018-10-05 05:51:38 +0000";
    previousSunset = "2018-10-04 17:04:09 +0000";
    sunrise = "2018-10-06 05:53:21 +0000";
    sunset = "2018-10-05 17:01:52 +0000";

You can retrieve the above schedule information from the command line in macOS Mojave:

$ /usr/bin/corebrightnessdiag sunschedule 

added in 2020: macOS Big Sur:

$ /usr/libexec/corebrightnessdiag sunschedule

Caveat is that CoreBrightness needs to know your location in order to calculate the correct sunset and sunrise times. If Wifi is disabled, CoreBrightness (via CLLocationManager) might not be able to determine your location. In that case, the calculated sun schedule is not correct. So make sure WiFi is turned on.

The private API to get this info with, is provided via an objective-c class called BrightnessSystemClient. To call this API with plain C, a bit of Objective-C runtime library magic is needed.

How to toggle dark/light mode

For the dark/light mode toggle, there does not seem to be any objective-c or plain C API available in macOS. Toggling is however possible programmatically, via AppleScript:

tell application "System Events"
    tell appearance preferences
      set dark mode to 1
    end tell
end tell

Since there is a way to directly execute AppleScript from objective-c (and hence from C via the Objective-C runtime), I decided to go for that route:

id scriptString = objc_msgSend((id)objc_getClass("NSString"),
                                sel_registerName("stringWithFormat:"), scriptString, darkMode);
id NSAppleScript = (id)objc_getClass("NSAppleScript");
SEL alloc = sel_registerName("alloc");
SEL init = sel_registerName("initWithSource:");
SEL release = sel_registerName("release");
id allocScript = objc_msgSend(NSAppleScript, alloc);
id scriptRef = objc_msgSend(allocScript, init, scriptString);
// Execute script    
id res = objc_msgSend(scriptRef, sel_registerName("executeAndReturnError:"), &err);

Running via launchctl

Last part is to run the application via launchctl. The is reasonable straightforward. Besides the mandatory .plist configuration file, I created a helper script to easly start, stop and restart the service.

updated_at 01-09-2019