iTerm2 Matrix Screensaver — Harder Than It Looks

Table of Contents

The Idea

Simple enough in concept: when a terminal window has been sitting idle for a while, run cmatrix as a screensaver. When you come back and click the window, it stops and you’re back at the prompt.

It’s a bit silly. But I spend a lot of time in terminal windows and I like the aesthetic. More importantly, it seemed like a two-minute job. It was not a two-minute job.

First Attempt: TMOUT

The obvious first move was TMOUT. Bash has a built-in idle timeout variable — set TMOUT=2700 in .bash_profile and bash will fire a signal after 45 minutes of inactivity.

The problem is that TMOUT without a TRAPALRM handler doesn’t run a command — it kills the shell session entirely. There’s a way to hook it properly in zsh, but I’m on bash 3.2 (the version Apple ships, because of GPL licensing), and getting a reliable trap handler that runs a command rather than exiting the session is fiddly. After a couple of failed attempts where it was silently terminating sessions, I commented it out and looked for a different approach.

iTerm2 Python API

iTerm2 has a Python scripting API that gives you programmatic access to terminal sessions. You can read screen content, send keystrokes, monitor focus events, and run code in response to any of it. Scripts placed in ~/Library/Application Support/iTerm2/Scripts/AutoLaunch/ start automatically with iTerm2.

That’s the right level to solve this. Instead of hooking into the shell, hook into the terminal emulator itself.

The script — screensaver.py — does two things concurrently using asyncio.gather():

poll_idle() — runs on a timer, takes an MD5 hash of the visible screen content for each session, and compares it to the previous hash. If the content hasn’t changed for longer than the idle threshold, it sends cmatrix -s -b\n to that session as a keystroke. No shell hook required.

watch_focus() — listens to iTerm2’s FocusMonitor for window focus events. When a window is clicked or brought to the front, it sends a space keystroke to any session running cmatrix, which kills it cleanly and returns to the prompt.

The Problems

Getting the basic loop working was straightforward. The edge cases were not.

It kept firing in Claude Code sessions

The first version fired cmatrix even when Claude Code was actively running — Claude’s output would scroll past, the hash would stop changing while Claude was thinking, and a few minutes later the terminal would go green. Sending a space to kill cmatrix also sent a space to Claude’s input, which was worse.

The fix was checking iTerm2’s jobName variable, which reports the foreground process in the session. First approach: blacklist — skip sessions where jobName contains “claude” or “node”. This mostly worked but felt fragile.

It kept firing in fzf

The blacklist missed fzf. __fzf_cd__ is the shell function that wraps fzf for directory navigation — jobName there shows as “fzf”, which wasn’t in the blacklist. The cmatrix text was being piped into fzf’s input, which produced creative results.

The allowlist fix

Instead of maintaining a blacklist of things to exclude, flip it: only fire when the foreground process is an idle shell. The SAFE_JOBS allowlist:

SAFE_JOBS = ("bash", "zsh", "fish", "sh")

If jobName is anything other than a bare shell — claude, node, fzf, vim, man, htop, whatever — the script skips that session entirely. This is the right approach. A blacklist grows forever; an allowlist means new tools are excluded automatically.

Enabling the API

The Python API requires two things in iTerm2 settings that aren’t on by default:

  1. Settings → General → Magic → Enable Python API — must be ticked
  2. The script must be ticked in Scripts → AutoLaunch menu — it appears there after being placed in the AutoLaunch directory, but is disabled until you enable it from the menu

Both of these are easy to miss and the script silently does nothing if either is off.

The Final Script

IDLE_MINUTES   = 45
IDLE_SECONDS   = IDLE_MINUTES * 60
CHECK_INTERVAL = 30   # poll interval in seconds

SAFE_JOBS = ("bash", "zsh", "fish", "sh")

The poll interval is 30 seconds — close enough to 45 minutes that the screensaver fires within half a minute of the threshold. Reducing it further has no real benefit and means more API calls.

cmatrix -s -b runs in screensaver mode (-s) with bold characters (-b). Press any key or click the window and it exits cleanly.

Setup

Requires iTerm2 with the Python API enabled and cmatrix installed:

brew install cmatrix

Then place screensaver.py in ~/Library/Application Support/iTerm2/Scripts/AutoLaunch/, restart iTerm2, and tick the script in Scripts → AutoLaunch.


Part of an ongoing effort to stop spending time on things that should just happen automatically. Sometimes the things that should take two minutes take two hours. That’s fine.