{ Wed, 26. Feb 2014 }

Using the MBP's Light Sensor to Change Emacs Theme

I was thinking about a good way to switch between the light and dark versions of Solarized today. Of course I can do it manually, but that’s tedious, and I know I’ll never do it. I could make it switch based on the time of day, but that’s boring, and anybody can do that.

Then the screen on my MacBook started dimming because my head once again got between the ceiling light and the ambient light sensor. And I had an idea. What if I can get a reading from the sensor, and use that to make Emacs switch? So I got to work.

We’ll need a couple of things. First of all, we need to get a reading from the sensor. Under Linux, the value is accessible via /sys, but for those of us actually running OS X on our MBPs, this doesn’t work. I did some digging, and found a post on Stack Overflow. I adjusted it a bit to get a single reading, and turned it into this (excuse my C):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// lmutracker.c
//
// clang -o lmutracker lmutracker.mm -framework IOKit -framework CoreFoundation

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

static io_connect_t dataPort = 0;

void getSensorReading() {
  kern_return_t kr;
  uint32_t outputs = 2;
  uint64_t values[outputs];

  kr = IOConnectCallMethod(dataPort, 0, NULL, 0, NULL, 0, values, &outputs, NULL, 0);
  if (kr == KERN_SUCCESS) {
        printf("%lld\n", values[0]);
    return;
  }

  if (kr == kIOReturnBusy) {
    return;
  }

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

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

  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);
  printf("%ld", 0L);
  
  getSensorReading();

    return(0);
}

Great, now we have a reading. Now we need to tell Emacs about it. To achieve this, I wrote a quick Bash script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash

DARKTHEME="solarized-dark"
LIGHTTHEME="solarized-light"
THRESHOLD=800000

function setlight {
    echo $1 > ~/.elight
}

function getlight {
    cat ~/.elight
}

if [ $(lmutracker) -lt ${THRESHOLD} ]; then
    [[ $(getlight) -eq 0 ]] || emacsclient --eval "(load-theme '${DARKTHEME} t)"
    setlight 0
else
    [[ $(getlight) -eq 1 ]] || emacsclient --eval "(load-theme '${LIGHTTHEME} t)"
    setlight 1
fi

Obviously, this requires emacs-server to be running. I’m saving the current state (“dark” or “light”) in a file to reduce unnecessary refreshes of Emacs. Now all that’s left is to periodically run this script. I do realize this method is hacky as hell, but it works, and it does so rather well as a POC.