jeff.sh makes gifs

We recently sunset open-sourced Jeff, which for years was our team’s go-to method for making GIF screen recordings and sharing them in Slack, Trello, GitHub or anywhere else. I’ve been using Monosnap in its place, pointed at an S3 bucket and subdomain because I had them available. I experimented with another tool too, though, that I wanted to share.

The new iOS Simulator in Xcode 9 has some additional commands you can run to control it from the command line. Searching for simctl will pull up some good guides to all of the things you can do with this command, but the one that caught my eye was being able to record a video of the screen. One of the things I would record most often with Jeff was a demo of a new feature I’d worked on to put in a GitHub PR. Maybe it would be possible to script this process with simctl?

If you try running xcrun simctl io booted recordVideo --type=mp4 recording.mp4 yourself you’ll notice that it starts recording and expects you to type ctrl+c in the terminal in order to stop. It wasn’t immediately clear how to use this command from within another script. The outer script would need to wait for simctl to finish its work before converting the video to a GIF and doing cleanup.

One way to wait for a command to finish is by running it in the background and then using the wait command. (Sounds almost too simple when I write it down like that…) A command can be set to run in the background with a trailing ampersand. Your shell will print the job number and process ID of the background job.

> sleep 10 &
[1] 74432

See what happens when you run sleep 10 &; echo “done” and then try it with wait: sleep 10 &; wait; echo “done”.

So if it’s possible to wait for the recording to finish, how do we send the background job a ctrl+c as if it were still in the foreground? Well, what does ctrl+c actually do? Your terminal is sending a POSIX signal to the command, in particular a SIGINT, and this can also be done programmatically. Maybe you’ve used the kill command to force-quit a command line tool or daemon, and it uses the same mechanism.

Try running sleep 1000 &; wait; echo “done” like before. Note the process ID that’s printed out, it’s the second number. This time, before 1000 seconds is up, run kill -INT process_id (put the number in place of process_id) and see what happens.

So now we can record the iOS simulator in the background, and it’s possible to stop the recording when we’re ready, but how do we use this script? Thinking about a typical workflow, we’d probably want to have some sort of record button, maybe in the menu bar, or a global hotkey that could be pressed to start and stop recording. How would a shell script be used like a toggle button?

Part of the problem is that there’s some state that needs to be tracked, particularly whether the script is currently recording something. The overall flow is something like: if the script is not recording, start recording. If the script is recording and it’s cancelled, stop and clean up. If it’s recording and it’s stopped, stop and convert the recording. Because the recording command is being run as a background job, it’s necessary to keep track of the process ID so it can be interrupted later, so it can serve as a token to represent this state. If the token exists, we’re live, otherwise we’re ready to start recording.

A really simple way to keep track of state with command line scripts is to put things in the filesystem, and doing this with process IDs is a common practice called pid files. Revisiting the workflow above, once recording starts and a process ID exists, the script can write it to a known file location like .jeff.pid.

Now that the state is stored somewhere, if the script is run again it can check for that pid file in order to know when to stop the recording. This is probably the most complex part of this script, because there’s going to be two instances of it running at this point. The first instance is still waiting for the recording to stop, and the second has detected that recording is occurring and is going to stop it. Maybe some pseudo-code will help:

if there's a pid file
  stop the recording # <- second instance is here
  exit
end

start recording
write pid file
wait for it to finish # <- first instance is here, it skipped the if/end
delete the pid file
convert it to a gif

I think this is the neatest part about this script! Now it can be used like a start/stop button however you’d prefer to use it. I’ve set it up with a global hotkey using khd.

I spent a bit of time iterating on this and polishing it, with things like notifications from terminal-notifier since otherwise there’s no UI for it. It’s not a full replacement for Jeff or Monodraw, but I learned a few useful things about shell scripting in the process, and I’ve been able to use it often at work already.

The finished script can be seen in my dotfiles.