2012-08-21

Reload a web browser when a file changes (Node.js, Grunt, Mac)

2012-08-24: JSReload now also supports Chrome/Mac.

This blog post explains how to use Node.js to reload a tab in Safari whenever a file changes. Alas, this solution only works on a Mac, because it relies on AppleScript to remotely control Safari. You can download it as project JSReload on GitHub.

The problem

I’m currently evaluating whether to switch from TextMate to Sublime Text. The only feature I miss on Sublime is a live preview of HTML files (or files that can be converted to HTML via a script – think Markdown).

The solutions

We’ll look at two solutions:
  1. A shell script (powered by Node.js) that watches a directory. It reloads Safari whenever one of the .html or .css files in the directory changes. In some ways that’s better than TextMate, because you can edit a CSS file and the corresponding HTML file is reloaded in Safari.
  2. A Grunt script that serves a different purpose: It watches a directory with AsciiDoc (.asc) files. When a file changes, that file is compiled to HTML and then Safari is reloaded.

Shell script

The shell script watchdir.js is written in JavaScript, via Node.js [1]. We use the Node.js function fs.watch() to be notified if a file changes:
    fs.watch(filename, [options], [listener])
The listener is called whenever the file changes whose name is filename. fs.watch has a few quirks on Mac OS X, but it mostly works. Then we still need to reload Safari. And AppleScript lets us do that:
    var code = '\
    tell application "Safari"\n\
        set mytab to first tab of first window\n\
        tell mytab to do javascript "window.location.reload()"\n\
    end tell\n\
    ';
We execute the AppleScript program in the variable code via the shell command osascript:
    spawn("osascript", ["-e", code])
Node.js makes sure that everything is properly quoted and escaped.

It is unfortunate that we have to resort to JavaScript inside the AppleScript, but that seems to be the only way to reload. A pure AppleScript solution is to set the URL of the tab to what it already is. That also reloads, but then the viewport jumps to the beginning of the file. With a true reload, the current viewport position is preserved.

Grunt

Grunt is similar to build tools such as Make and Ant. But it is based on JavaScript and Node.js, which makes it a very elegant solution. The following is a snippet from the build file grunt.js:
    grunt.initConfig({
        asciidoc: {
            files: [ '**/*.asc' ]
        },
        watch: {
            files: '<config:asciidoc.files>',
            tasks: 'asciidoc reloadsafari'
        },
    });
We watch all AsciiDoc files (as specified via asciidoc.files). If one of them changes, we first convert it to HTML via the asciidoc shell command and then reload Safari. Both asciidoc and reloadsafari are custom Grunt tasks (included in the GitHub project). Hence, it should be easy to adapt this solution to different needs (e.g. Markdown).

Other browsers

Safari is perfect for previewing files for me, because I don’t use it for anything else. But other browsers can probably be remote-controlled via Node.js, too. For example, via the following mechanisms: If you don’t want to use scripts or the command line then you can use the Firefox add-on “Auto Reload” by Yurii Zolot'ko. Alas, Chrome doesn’t have anything similar, only extensions that reload periodically, which I don’t consider good enough. Lastly, there is the impressive Live.js, but for that you need to insert a script tag into your HTML file. And you have to access your HTML via a server.

Conclusion: an app wish

The presented solution works quite well. But Sublime Text should really provide its own preview functionality. There seems to be an aversion to putting too much functionality into Sublime. Then how about a separate application? It would show a single browser window and watch a directory. You would be able to configure what files are watched and how they are converted to HTML. It should be possible to optionally make that conversion without creating files, by writing to stdout. Example configurations:
  • HTML: Watch all .html and .css files. If one of them changes, reload the browser window.
  • AsciiDoc: Watch all .asc files. If one of them changes, convert it to an HTML file and reload the browser window.
  • Markdown: Watch all .md, .mdown and .markdown files. If one of them changes, translate it to HTML and pipe the result to the browser window.
If you find this idea appealing then you can vote for it on UserEcho (the official way of giving feedback for Sublime Text).

Reference

  1. Write your shell scripts in JavaScript, via Node.js

No comments: