Post image for Watcher ServiceAfter my epic three-part post on Saturday, I spent the rest of the weekend doing more “useful” things. Now it’s Sunday night (Monday morning, I think), and I’ve got some kind of minor food poisoning which is currently keeping me awake. Thus a “hey, cool trick” post.

I actually already own an app which can do this to some extent, and I know there are more available. I like to do things the hard way once in a while. What I wanted was a basic script which could execute arbitrary code whenever a file of a certain type changed within a directory. The use case is web development: whenever I change a site-related file (html, php, css, less, rb, erb, etc.), I want Safari to refresh the related page.

Folder Actions don’t work well on my system (do they work for anyone?). Hazel would work, but I needed something more immediate. I had a version that used launchd, but it was difficult to consistently start and stop from a script. Here’s the final solution I came up with.

At the top, let me say that the heavy lifting in my script was taken from a SASS file-watching script by Carlo Zottman. It uses Ruby to poll a file collection for modification date variations, and keeps a pretty low profile. I wanted to avoid compiled code for this, as I eventually do want to go to bed tonight.

The basic goals:

  • Watch only files of a specific type
  • Refresh my primary browser when a change occurs
  • Refresh across windows and tabs, but limit by user-specified keyword in URL

I built both a command line script and a System Service to do this, and both work as standalone solutions. The Automator action makes it possible to right click a folder in Finder and choose “Watcher” to start watching it, and it asks you for the tab keyword in a nice popup dialog. Beyond that, it really just wraps the command line script. You can modify either with the following instructions.

Here’s the command line version:

#!/usr/bin/env ruby
# watch.rb by Brett Terpstra, 2011 <http://brettterpstra.com>
# with credit to Carlo Zottmann <https://github.com/carlo/haml-sass-file-watcher>

trap("SIGINT") { exit }

if ARGV.length < 2
  puts "Usage: #{$0} watch_folder keyword"
  puts "Example: #{$0} . mywebproject"
  exit
end

dev_extension = 'dev'
filetypes = ['css','html','htm','php','rb','erb','less','js']
watch_folder = ARGV[0]
keyword = ARGV[1]
puts "Watching #{watch_folder} and subfolders for changes in project files..."

while true do
  files = []
  filetypes.each {|type|
    files += Dir.glob( File.join( watch_folder, "**", "*.#{type}" ) )
  }
  new_hash = files.collect {|f| [ f, File.stat(f).mtime.to_i ] }
  hash ||= new_hash
  diff_hash = new_hash - hash

  unless diff_hash.empty?
    hash = new_hash

    diff_hash.each do |df|
      puts "Detected change in #{df[0]}, refreshing"
      %x{osascript<<ENDGAME
        	tell application "Safari"
          	set windowList to every window
          	repeat with aWindow in windowList
          		set tabList to every tab of aWindow
          		repeat with atab in tabList
          			if (URL of atab contains "#{keyword}") then
          			  tell atab to do javascript "window.location.reload()"
          			end if
          		end repeat
          	end repeat
        	end tell
ENDGAME
}
    end
  end

  sleep 1
end

Installing

If you want to use the above script from Terminal, just put it in a directory in your path and run chmod a+x watch.rb on it. Then you can call it with watch.rb folder/to/watch keyword. The keyword you pass will determine which tabs will refresh in your browser. For example, if I’m working on dev.heckyesmarkdown.com (my local development version), I would use “dev.heckyes” to limit the refresh to only associated tabs. Once the script is running, you can stop it any time by typing Control-C in that Terminal. If you’ve run it in the background, you’ll either need to foreground it or kill it manually.

To install the System Service, download the workflow, unzip it and place it in ~/Library/Services (where ‘~’ is your home folder). It will now show up when you right click on one or more selected folders in the Finder. Choose “Watcher,” enter a URL-matching keyword and let it go. You should see the spinning gear icon in your menubar. When you want to stop watching the folder, click that icon and choose “Stop Watcher”.

Customizing

In the standalone Ruby script (above), you can easily modify the watched filetypes in line 14, and you can replace the AppleScript with your own starting on line 34. I’ll offer some examples for other browsers below.

The Service contains pretty much the same script, but modified to work with an Automator workflow. If you open it in Automator and skip to the last action, you’ll see the script and you can make your filetype and AppleScript modifications in there.

Using other browsers

Chrome has decent AppleScript support these days, so changing this script to work with it is trivial. Just replace the AppleScript portion (beginning with “tell application…”) with the following:

tell application "Google Chrome"
	set windowList to every window
	repeat with aWindow in windowList
		set tabList to every tab of aWindow
		repeat with atab in tabList
			if (URL of atab contains "#{keyword}") then
				tell atab to reload
			end if
		end repeat
	end repeat
end tell

Firefox is a little less elegant, as far as I know, and requires System Events scripting to refresh a page. If you know a better way, I’d love to hear it, but here’s a basic script for reloading the front page. You might as well delete the keyword portions of the script/workflow if you go this route, they won’t be applicable:

 tell app "Firefox" to activate
 tell app "System Events"
   keystroke "r" using command down
 end tell

I won’t detail any other browsers; if you’re doing web development in something more exotic, I’ll assume you know how to script it.

Executing arbitrary code

You don’t have to refresh browsers with this. You don’t even have to use it for web development. Have it watch text files for changes and run an iCal script when one is modified. It’s basically a hyperactive Folder Action.

If you want to have the script do something new every time you use it, you might want to externalize the action code. Just modify the diff_hash.each do |df| block to run an outside script. If it’s shell code, just make it executable and call it with %x{path/to/script}, and if it’s AppleScript, call it with %x{/usr/bin/osascript path/to/script}. Then you can modify the action script each time without changing the watch.rb file.

If you want to get really crazy, you could pass the script to execute as a command line parameter, or request it in the Automator workflow and pass it in. I don’t have a need for that right now, but if you build it, let me know!

I hope folks find this useful. Now, excuse me while I go retch for a little bit.

Download

Watcher Service v1

A Snow Leopard System Service that runs on folders in Finder. By default, it watches for changes to web dev files and refreshes Safari, but it can be customized extensively. See the "more info" link for customization instructions.

Published 03/07/11.

Updated 03/07/11. Changelog

DonateMore info…