Skip to content

Instantly share code, notes, and snippets.

@jerblack
Last active October 2, 2023 17:08
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jerblack/869a303d1a604171bf8f00bbbefa59c2 to your computer and use it in GitHub Desktop.
Save jerblack/869a303d1a604171bf8f00bbbefa59c2 to your computer and use it in GitHub Desktop.
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
}
// MIT License
// Copyright 2022 Jeremy Black
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@lfuelling
Copy link

Would you be willing to license this code as MIT (same as with the windows example)?

@jerblack
Copy link
Author

jerblack commented Oct 9, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment