Darius Foo

Watching the screen for changes in macOS

Sometimes all you need is for your computer to tell you when something on the screen changes. Maybe you're invigilating an exam over Zoom and your eyes are drying out from staring at the chat window, which for some reason doesn't have any notification settings.

Rectangles

The first piece is how to capture a small region of the screen. Interestingly, this is built into the screencapture command, which takes the coordinates and dimensions of a rectangular area to capture.

We could fudge around until we get the right coordinates, but there's a better way in AppKit. This tiny Objective-C program (from here) prints the current coordinates of the mouse (with the origin at the top-left).

#import <AppKit/AppKit.h>

int main (int argc, const char * argv[]) {
   NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

   NSRect e = [[NSScreen mainScreen] frame];
   int H = e.size.height;

   NSPoint mouseLoc = [NSEvent mouseLocation];
   // origin is now at the top left
   NSString* locString = [NSString stringWithFormat:@"%d\n%d", (int) mouseLoc.x, H - (int) mouseLoc.y];
   printf("%s\n", [locString UTF8String]);

   [pool drain];
   return 0;
}
gcc -o MouseLocation MouseLocation.m -framework AppKit

Notifications

We also need a way to show a notification. A simple way is to use iTerm-specific escape codes; we simply surround the text we want to show with them.

notify() {
  echo $'\e]9;'$1$'\007'
}

We could also use AppleScript, which allows us to provide a title.

notify() {
  osascript -e "display notification \"$2\" with title \"$1\""
}

Glue

Everything is glued together with this terrible script:

#!/usr/bin/env bash

tlx=$1
tly=$2
brx=$3
bry=$4

capture() {
  screencapture -x -R $tlx,$tly,$(($brx-$tlx)),$(($bry-$tly)) /tmp/screen.png
}

set -x

capture
res="$(md5 /tmp/screen.png)"
prev="$res"

while true; do
  sleep 5
  capture
  res="$(md5 /tmp/screen.png)"
  if [ "$res" != "$prev" ]; then
    notify "Something changed!"
    exit 0
  fi
  prev="$res"
done

The arguments are the coordinates we get from running ./MouseLocation twice, with the mouse cursor at the top-left and bottom-right of the rectangle we want to watch.

One thing that appears silly to do (and is very much in the spirit of this post) is the use of MD5 for comparing images. We want to be notified if anything changes, and for this use case a more sophisticated image diff like ImageMagick's compare wouldn't add much. screencapture is thankfully deterministic as well, so this all works. For something more resilient to small variations, you might want to use one of the metrics offered by ImageMagick.

Verdict

This is really janky but works surprisingly well.