Setting Emacs Theme Based on Ambient Light

I sit next to a window at work. On sunny days it’s easier to see a light editor theme, and when the sky is dark, a dark theme is easier on my eyes. So I decided to try to have my MacBook automatically switch the Emacs theme based on readings from the ambient light sensor.

Demonstration of the theme switching in action

There are two parts to this solution: a command-line executable to read data from the sensor, and then a small elisp function to do the theme switching.

The program below is from StackOverflow and slightly modified. It gets the AppleLMUController IO service, then, when the service is ready, prints the light sensor data to stdout and exits.

// lmutracker.mm
//
// clang -o lmutracker lmutracker.mm -framework IOKit -framework CoreFoundation

#include <mach/mach.h>
#import <IOKit/IOKitLib.h>
#import <CoreFoundation/CoreFoundation.h>

static double updateInterval = 0.1;
static io_connect_t dataPort = 0;

void updateTimerCallBack(CFRunLoopTimerRef timer, void *info) {
  kern_return_t kr;
  uint32_t outputs = 2;
  uint64_t values[outputs];

  kr = IOConnectCallMethod(dataPort, 0, nil, 0, nil, 0, values, &outputs, nil, 0);
  if (kr == KERN_SUCCESS) {
    printf("%8lld", values[0]);
    exit(0);
  }

  if (kr == kIOReturnBusy) {
    return;
  }

  mach_error("I/O Kit error:", kr);
  exit(kr);
}

int main(void) {
  kern_return_t kr;
  io_service_t serviceObject;
  CFRunLoopTimerRef updateTimer;

  serviceObject = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController"));
  if (!serviceObject) {
    fprintf(stderr, "failed to find ambient light sensors\n");
    exit(1);
  }

  kr = IOServiceOpen(serviceObject, mach_task_self(), 0, &dataPort);
  IOObjectRelease(serviceObject);
  if (kr != KERN_SUCCESS) {
    mach_error("IOServiceOpen:", kr);
    exit(kr);
  }

  setbuf(stdout, NULL);

  updateTimer = CFRunLoopTimerCreate(kCFAllocatorDefault,
                  CFAbsoluteTimeGetCurrent() + updateInterval, updateInterval,
                  0, 0, updateTimerCallBack, NULL);
  CFRunLoopAddTimer(CFRunLoopGetCurrent(), updateTimer, kCFRunLoopDefaultMode);
  CFRunLoopRun();

  exit(0);
}

The accompanying elisp code will invoke that executable on a timer and change the theme based on the light reading.

(setq current-theme "dark")
(defconst light-theme 'majapahit-light)
(defconst dark-theme 'majapahit-dark)

;; will apply a dark theme if the room is dark, and a light theme if the room is
;; bright
(defun change-theme-for-lighting ()
  (let* ((current-light-sensor-reading
          (string-to-number
           (shell-command-to-string "./lmutracker"))))
    (if (< current-light-sensor-reading 100000)
        (when (not (string-equal current-theme "dark"))
          (load-theme dark-theme 1)
          (setq current-theme "dark"))
      (when (not (string-equal current-theme "light"))
        (load-theme light-theme 1)
        (setq current-theme "light")))))

;; probably want to run this less frequently than every second
(run-with-timer 0 1 #'change-theme-for-lighting)