This post is about interacting with the graphical desktop environment from a shell script. Specifically: say you have a long-running bash script and you’d like to keep an eye on it through a unity app indicator. How do you make that work in an appropriately unix-y way? (If you’re not sure what I mean see the animation below).
Background. Over the weekend I finally changed from NFS to rsync for keeping my source code in sync across machines. Much better if my network connection is unreliable and fewer moving parts. After I was done with the basics though I thought: I really need to be able to keep an eye on how syncing is going. It’s just the worst to spend ages unsuccessfully trying to reproduce an issue on a virtual machine only to realize that it’s because the code I’m running isn’t being properly synced when I edit files on my laptop.
Here’s what I had in mind: when I run the code change monitor shell script I want to get an app indicator in the top bar that changes state when a sync is going on, so I can see that syncing actually happening and whether it’s slow. And I want it to show if there are any errors. This, in other words
The syncing logic itself is straightforward: one component uses
inotifywait to wait for files to change (
watchdog.sh), then calls out to a script that does the actual sync (
How to show the indicator though? There is no command-line api for this kind of thing and I wasn’t sure what it would look like if there was. Also, the existing code was properly unix-y and I wanted it to stay that way. One component knows how to wait for changes but knows nothing about rsync; the rsync part knows how to do a single sync but doesn’t care when or why it is being called. They only meet at the top level,
watchdog.sh --root . ./do-rsync.sh
What I ended up with was a python script,
sync-indicator.py, that was responsible for just the indicator. When it starts it creates the indicator and then waits around, reading input from stdin. When it sees a command in the output it recognizes, say
[SYNC: syncing], it consumes that line and changes the state of the indicator. Anything that doesn’t match is just passes straight through to stdout. Here’s what it looks like,
This way the indicator logic doesn’t need to know about the watching or rsyncing, and those parts don’t need to know about the indicator. All you need to do is change the toplevel command that puts it all together to,
watchdog.sh --root . ./do-sync-and-indicate.sh | ./sync-indicator.py
do-sync-and-indicate.sh is a trivial wrapper around
do-sync.sh that looks like this,
#!/bin/bash echo "[SYNC: syncing]" if ./do-sync.sh; then echo "[SYNC: idle]" else echo "[SYNC: error]" fi
You can even replace this with a generic command that takes another command as an argument and all it does is call that command surrounded with appropriate
Pretty clean and unix-y I think, independent components and so on. The python indicator script itself, sync-indicator.py, is straightforward, only 60ish lines with comments.
The ease of supporting this makes me wonder though. Why aren’t long-running commands better at interacting with the desktop?