Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Golang: Detect dark mode change in OSX

I wanted to be able to detect when Dark mode was turned on or off in my Go program so I can swap icons in my tray app, but the Apple-blessed methods all use Swift and ObjectiveC. To work around this I figured out the following:

  • When dark mode is set, a property called "AppleInterfaceStyle" is set to "Dark" for the user.
  • When dark mode is disabled, that property is removed.
  • You can see this by running "defaults read -g AppleInterfaceStyle"
// Dark Mode On  
defaults read -g AppleInterfaceStyle  
 Dark  
echo $? // <- check error code  
 0  
  
// Dark Mode Off  
defaults read -g AppleInterfaceStyle  
 2019-12-08 00:50:27.106 defaults[37703:790070]  
 The domain/default pair of (kCFPreferencesAnyApplication, AppleInterfaceStyle) does not exist  
echo $? // <- check error code  
 1  

Global properties for the user are stored in ~/Library/Preferences/.GlobalPreferences.plist.
When the property is set, the file is truncated and rewritten by the OS, which results in a REMOVE and CREATE filesystem event. Since I didn't want to just poll the setting every few seconds since that seemed wasteful, I wanted to use a filesystem event watcher like what is provided with the Go package fsnotify.
I used this package to monitor the plist file for CREATE events and then when they occurred I called the defaults command shown above and checked the error code to determine whether or not dark mode was enabled.
When the dark mode state changed, the OS writes the change to the file and the file system watcher detects that and calls a separate function to react to the change.
Because it's a global property store, it's likely that it will be updated for more than just changes to dark mode, so it's important to track the previous state and only react when the state has actually changed.

package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"log"
"os"
"os/exec"
"path/filepath"
)
const plistPath = `/Library/Preferences/.GlobalPreferences.plist`
var plist = filepath.Join(os.Getenv("HOME"), plistPath)
var wasDark bool
func main() {
// get initial state
wasDark = checkDarkMode()
// Start watcher and give it a function to call when the state changes
startWatcher(react)
}
// react to the change
func react(isDark bool) {
if isDark {
fmt.Println("Dark Mode ON")
} else {
fmt.Println("Dark Mode OFF")
}
}
func checkDarkMode() bool {
cmd := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle")
if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
return false
}
}
return true
}
func startWatcher(fn func(bool)) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
isDark := checkDarkMode()
if isDark && !wasDark{
fn(isDark)
wasDark = isDark
}
if !isDark && wasDark {
fn(isDark)
wasDark = isDark
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
err = watcher.Add(plist)
if err != nil {
log.Fatal(err)
}
<-done
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.